Skip to content

feat: enhance Earn feature with EVM support and LayerSwap integration - #663

Merged
Dprof-in-tech merged 5 commits into
mainfrom
feat/noblocks-earn-evm-phase-2
Aug 6, 2026
Merged

feat: enhance Earn feature with EVM support and LayerSwap integration#663
Dprof-in-tech merged 5 commits into
mainfrom
feat/noblocks-earn-evm-phase-2

Conversation

@sundayonah

@sundayonah sundayonah commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds Phase 2 Earn: users on supported EVM chains (Base, Polygon, Arbitrum, etc.) can deposit USDC via LayerSwap into Vesu on Starknet, view their position on the source chain only, and withdraw back to that same chain — without holding ETH or STRK for gas.

Background: Phase 1 Earn (Starknet-native Vesu deposit/withdraw) already existed. This PR extends Earn to EVM source chains by bridging through LayerSwap while keeping the UX gasless and chain-scoped per product user stories #3#5.

What changed:

  • Deposit (EVM → Vesu): One sponsored EVM signature (EIP-7702 batch via existing bundler) for approve + LayerSwap deposit actions; background polling until bridge completes; Vesu supply on Starknet via AVNU paymaster using LayerSwap receive_amount (post-bridge-fee).
  • Withdraw (Vesu → source EVM): Vesu exit on Starknet (paymaster) → LayerSwap Starknet → EVM reverse bridge (paymaster via new /api/earn/layerswap/starknet-deposit) → poll until funds arrive on source chain; clears chain-scoped local position.
  • Chain-scoped UX (Revert standalone status page #4): Positions and activity are keyed/filtered by sourceChain (earnPositionStore, useEarnSourcePosition, filterEarnActivityForChain). EVM wallet views no longer show the global Starknet Vesu balance on unrelated chains.
  • LayerSwap integration: Server client in layerswap.ts; six Next.js API routes under /api/earn/layerswap/* (quote, swap, status, withdraw-quote, withdraw-swap, starknet-deposit); config keys layerswapApiKey / layerswapApiBaseUrl in config.ts.
  • Bug fixes during implementation:
    • Replaced raw Privy eth_sendTransaction with executeBatchCalls so ERC-20 approve calldata is not corrupted by dataSuffix.
    • Pending bridge jobs now store both requestedAmountBaseUnits and receiveAmountBaseUnits so resume/deposit uses the bridged amount, not the source send amount.
    • UI shows estimated Starknet receive and bridge fees from LayerSwap quote.

Feature flags / env (off by default):

  • NEXT_PUBLIC_EVM_EARN_ENABLED=true
  • LAYERSWAP_API_KEY (server-side)
  • LAYERSWAP_API_BASE_URL (optional; defaults to https://api.layerswap.io)

Breaking changes: None when flags remain false. Starknet-native Earn behavior unchanged.

Alternatives considered:

  • Withdraw to Starknet wallet only — simpler but violates user story Display real-time wallet balance in the navbar #5; replaced with full reverse LayerSwap path.
  • Direct on-chain bridge contracts — LayerSwap chosen for multi-chain support and existing quote/swap API.
  • Consolidated LayerSwap catch-all route — kept one route per URL to match existing /api/starknet/earn/* pattern.

API / contracts: New internal Noblocks routes only; no aggregator or on-chain contract changes. LayerSwap fees apply on bridge (especially on small amounts).


References

https://paycrest-io.atlassian.net/jira/software/projects/KAN/boards/3?selectedIssue=KAN-394


Testing

Unit tests added/updated:

  • __tests__/earnFeature.test.ts — feature flags, chain-scoped activity filter (incl. legacy untagged deposits)
  • __tests__/earnChains.test.ts — supported EVM source chains / LayerSwap network map
  • __tests__/layerswapStarknetExecute.test.ts — LayerSwap Starknet deposit_actions → Starknet Call[] parsing

Manual E2E (Base, recommended):

  1. Set env: NEXT_PUBLIC_EARN_ENABLED=true, NEXT_PUBLIC_EVM_EARN_ENABLED=true, LAYERSWAP_API_KEY, paymaster + bundler keys.
  2. Switch wallet to Base → open Earn → enter USDC amount → confirm quote shows estimated Starknet receive and fees.
  3. Deposit → one EVM signature → wait for bridge (~15 min) → Vesu position appears on Base only (not Polygon/Arbitrum).
  4. Switch to another EVM chain → Earn hub shows no Base position/activity.
  5. Withdraw on Base → funds return to Base USDC balance (not Starknet wallet); position clears on Base; activity remains scoped.
  6. Refresh mid-bridge → pending job resumes Vesu deposit with receive amount.
  • This change adds test coverage for new/changed/fixed functionality

Checklist

  • I have added documentation and tests for new/changed functionality in this PR (.env.example entries + unit tests)
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main
  • N/A — no database migration

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features
    • Added Earn support for eligible EVM networks, enabling USDC deposits and withdrawals through Starknet.
    • Added LayerSwap bridge quotes, estimated completion times, status tracking, confirmations, and automatic recovery of pending transfers.
    • Added source-chain balances, APY, activity filtering, wallet total updates, and network-specific availability messaging.
  • Bug Fixes
    • Improved handling of unavailable networks, legacy activity, invalid bridge data, ownership validation, and failed transactions.
  • Tests
    • Added coverage for supported chains, bridge flows, feature visibility, activity filtering, position tracking, authentication, and transaction preparation.

- Added configuration options for EVM Earn and LayerSwap API in .env.example and config.ts.
- Introduced new components and hooks to manage EVM Earn flows, including EarnBridgeTracker and useEvmEarnHandler.
- Updated existing components (e.g., EarnActivityPanel, EarnWalletForm) to support EVM-specific logic and display.
- Implemented filtering for earn activity based on the source chain, allowing for better user experience across different networks.
- Enhanced UI elements to indicate when Earn is unavailable on certain networks, improving clarity for users.
- Refactored related types and utility functions to accommodate new EVM features and ensure type safety.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6893c332-052f-4ef8-8315-7d763b11cb60

📥 Commits

Reviewing files that changed from the base of the PR and between bd387e0 and c4007dd.

📒 Files selected for processing (7)
  • __tests__/evmEarnWalletTotal.test.ts
  • app/components/EarnDisclosureBanner.tsx
  • app/components/MobileDropdown.tsx
  • app/components/WalletDetails.tsx
  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/hooks/useEvmWalletDisplayTotal.ts
  • app/lib/evmEarnWalletTotal.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/components/EarnDisclosureBanner.tsx
  • app/hooks/useEvmWalletDisplayTotal.ts
  • tests/evmEarnWalletTotal.test.ts
  • app/components/WalletDetails.tsx
  • app/lib/evmEarnWalletTotal.ts
  • app/components/MobileDropdown.tsx
  • app/components/wallet-mobile-modal/WalletView.tsx

📝 Walkthrough

Walkthrough

Adds EVM-to-Starknet Earn through LayerSwap and Vesu. The change adds chain configuration, API routes, bridge execution, persistent recovery, feature gating, activity filtering, and network-aware wallet interfaces.

Changes

EVM-to-Starknet Earn

Layer / File(s) Summary
Contracts, gating, and persisted state
.env.example, app/lib/config.ts, app/types.ts, app/lib/earnChains.ts, app/lib/earnFeature.ts, app/lib/earnPositionStore.ts, app/hooks/useEarnHandler.ts, __tests__/earn*
Defines supported EVM chains, LayerSwap mappings, configuration, feature visibility, chain-scoped activity, source positions, pending bridge jobs, and source-chain transaction metadata.
LayerSwap API and Starknet execution
app/lib/layerswap*, app/api/earn/layerswap/..., __tests__/layerswap*
Adds quote, swap, status, withdrawal, Starknet deposit, action conversion, batch-call, authentication, validation, and upstream-error handling.
EVM flow and bridge recovery
app/hooks/useEvmEarnHandler.ts, app/hooks/useEarnSourcePosition.ts, app/hooks/useEarnBridgeStatusTracker.ts, app/components/EarnBridgeTracker.tsx, app/providers.tsx
Implements EVM deposit and withdrawal flows, LayerSwap polling, Vesu operations, source-position persistence, pending-bridge recovery, and provider mounting.
Network-aware Earn interface
app/components/EarnWalletForm.tsx, app/components/wallet-mobile-modal/*, app/components/EarnActivityPanel.tsx, app/components/EarnSourcePositionCard.tsx, app/components/WalletDetails.tsx, app/components/MobileDropdown.tsx, app/components/EarnUnavailableModal.tsx, app/hooks/useEvmWalletDisplayTotal.ts, app/lib/evmEarnWalletTotal.ts
Updates wallet actions, EVM balances, bridge quotes, confirmation states, activity filtering, source-position display, and unavailable-network messaging.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: onahprosper, 5ran6

Poem

I hop through chains with USDC bright,
LayerSwap carries it through the night.
Vesu receives each careful flow,
Bridge jobs wait while statuses grow.
New Earn paths bloom from screen to store. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: EVM Earn support through LayerSwap.
Description check ✅ Passed The description covers purpose, background, implementation, APIs, testing, references, flags, alternatives, and migration impact; environment details are only partially specified.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (6)
__tests__/layerswapStarknetExecute.test.ts (1)

3-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the throwing branches.

layerswapDepositActionsToStarknetCalls gates a transaction-signing path, and every failure mode throws. Add cases for invalid JSON, a call that omits entrypoint, a call_data value of "0x", and multiple actions with out-of-order order values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/layerswapStarknetExecute.test.ts` around lines 3 - 26, Extend the
layerswapDepositActionsToStarknetCalls tests with cases asserting throws for
invalid JSON, missing entrypoint, and call_data equal to "0x". Add a
multiple-action case with out-of-order order values and assert the resulting
calls follow the expected order.
app/lib/layerswap.ts (2)

76-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Preserve the upstream HTTP status instead of discarding it.

validateStatus: () => true makes axios resolve for 401, 429, and 5xx responses. The code then only reads data.error?.message. If LayerSwap returns an error body in another shape, or an HTML gateway page, data.error is undefined and the helper throws "LayerSwap quote unavailable". An expired API key and a rate limit then look identical to a missing quote, and every caller maps the result to 502.

Include the status in the thrown error, or drop validateStatus and handle AxiosError.response.status. The same option appears at lines 133, 182, and 204.

♻️ Proposed status propagation
-  const { data } = await axios.get<LayerswapApiResponse<LayerswapQuote>>(
+  const { data, status } = await axios.get<LayerswapApiResponse<LayerswapQuote>>(
     `${LAYERSWAP_API_BASE}/api/v2/quote`,
@@
   if (data.error?.message) {
     throw new Error(data.error.message);
   }
   if (!data.data) {
-    throw new Error("LayerSwap quote unavailable");
+    throw new Error(`LayerSwap quote unavailable (upstream status ${status})`);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/layerswap.ts` around lines 76 - 99, Update the LayerSwap request
error handling around the quote-fetching method and the other requests using
validateStatus: () => true to preserve and propagate the upstream HTTP status.
Include the response status when throwing for error responses, including bodies
without data.error or non-JSON gateway responses, so callers can distinguish
authentication, rate-limit, server, and unavailable-quote failures instead of
mapping them all to 502.

261-270: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate calldata element types.

c.calldata is accepted without a type check. A payload such as {"contractAddress":"0x1","entrypoint":"transfer","calldata":[{"a":1}]} passes this loop and fails later inside the Starknet account, which produces an opaque error at transaction build time.

Reject non-string entries here.

♻️ Proposed shape check
       if (!c.contractAddress || !c.entrypoint) {
         throw new Error("LayerSwap Starknet call is missing fields");
       }
+      const calldata = c.calldata ?? [];
+      if (
+        !Array.isArray(calldata) ||
+        calldata.some((v) => typeof v !== "string")
+      ) {
+        throw new Error("LayerSwap Starknet call has invalid calldata");
+      }
       calls.push({
         contractAddress: c.contractAddress,
         entrypoint: c.entrypoint,
-        calldata: c.calldata ?? [],
+        calldata,
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/layerswap.ts` around lines 261 - 270, Update the validation in the
loop processing parsed LayerSwap calls, alongside the existing contractAddress
and entrypoint checks, to require that every c.calldata element is a string.
Reject any non-string calldata entry with the same early validation error before
pushing the call, while preserving the existing default of [] when calldata is
absent.
app/providers.tsx (1)

127-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Gate the tracker on the feature flag.

EarnBridgeTracker mounts on every page and installs a 15-second interval. It runs even when config.evmEarnEnabled is false, so a disabled feature can still resume a persisted bridge job and call the Earn deposit path. Mount it only when the flag is on.

♻️ Proposed gating
-                      <EarnBridgeTracker />
+                      {config.evmEarnEnabled && <EarnBridgeTracker />}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/providers.tsx` at line 127, Update the provider rendering around
EarnBridgeTracker so it mounts only when config.evmEarnEnabled is true. Preserve
the existing tracker behavior and placement when the feature flag is enabled,
and render nothing for the disabled case.
app/hooks/useEarnSourcePosition.ts (1)

19-21: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Drop the localStorage read from the state initializer.

The server render produces null because readEarnSourcePosition guards on typeof window. The first client render calls this initializer and can return a stored position. EarnSourcePositionCard then renders a card where the server rendered nothing, which causes a hydration mismatch. The mount effect at line 31 already hydrates the value.

♻️ Proposed fix
-  const [position, setPosition] = useState<EarnSourcePosition | null>(() =>
-    evmAddress ? readEarnSourcePosition(evmAddress, sourceChain, token) : null,
-  );
+  const [position, setPosition] = useState<EarnSourcePosition | null>(null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/hooks/useEarnSourcePosition.ts` around lines 19 - 21, Remove the
localStorage-backed read from the useState initializer in useEarnSourcePosition,
initializing position to null unconditionally. Keep the existing mount effect
responsible for calling readEarnSourcePosition and hydrating the stored value
after client mount.
app/components/EarnWalletForm.tsx (1)

188-206: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Clear the stale quote when the amount becomes invalid.

Both effects return before calling the fetchers when amountString is empty or non-positive. quote and withdrawQuote keep the previous values. The blocks at lines 554 and 534 render whenever amountEntered is true, so the receive estimate and the projection at line 211 can describe an earlier amount.

Call the fetcher with 0 in the early-return branch. fetchQuote and fetchWithdrawQuote already reset their state for non-positive input.

♻️ Proposed fix
   useEffect(() => {
-    if (!isEvmFlow || tab !== "deposit" || !amountString) return;
+    if (!isEvmFlow || tab !== "deposit") return;
     const human = parseFloat(amountString);
-    if (!(human > 0)) return;
+    if (!(human > 0)) {
+      void fetchQuote(0);
+      return;
+    }
     const id = window.setTimeout(() => {
       void fetchQuote(human);
     }, 400);
     return () => clearTimeout(id);
   }, [amountString, fetchQuote, isEvmFlow, tab]);
 
   useEffect(() => {
-    if (!isEvmFlow || tab !== "withdraw" || !amountString) return;
+    if (!isEvmFlow || tab !== "withdraw") return;
     const human = parseFloat(amountString);
-    if (!(human > 0)) return;
+    if (!(human > 0)) {
+      void fetchWithdrawQuote(0);
+      return;
+    }
     const id = window.setTimeout(() => {
       void fetchWithdrawQuote(human);
     }, 400);
     return () => clearTimeout(id);
   }, [amountString, fetchWithdrawQuote, isEvmFlow, tab]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/EarnWalletForm.tsx` around lines 188 - 206, Update both
effects around fetchQuote and fetchWithdrawQuote so empty or non-positive
amountString values invoke the corresponding fetcher with 0 before returning,
clearing stale quote state. Preserve the existing positive-amount debounce
behavior and dependency arrays.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 100-103: Validate LAYERSWAP_API_BASE_URL before any authenticated
LayerSwap requests, allowing only URLs whose protocol is https: and rejecting
all other protocols. Apply this validation in the LayerSwap
configuration/request flow that consumes LAYERSWAP_API_BASE_URL, while
preserving the existing default URL behavior.

In `@app/api/earn/layerswap/quote/route.ts`:
- Around line 16-24: Replace parseFloat with Number in both layerswap quote
routes, and validate amount with Number.isFinite(amount) && amount > 0 before
calling layerswapGetQuote; preserve the existing 400 response for invalid input.
Update app/api/earn/layerswap/quote/route.ts (lines 16-24) and
app/api/earn/layerswap/withdraw-quote/route.ts (lines 20-31) with HTTP 400 tests
covering partially parsed values such as "1abc" and non-finite values such as
"Infinity".

In `@app/api/earn/layerswap/starknet-deposit/route.ts`:
- Around line 114-120: Update the receipt wait in the Starknet deposit route
around account.waitForTransaction to use the supported retry or timeout option
so a stuck transaction cannot block indefinitely. In the txReceipt.isSuccess()
failure response, preserve the existing error and status while also returning
result.transaction_hash for client reconciliation.

In `@app/api/earn/layerswap/swap/route.ts`:
- Around line 17-30: Move request.json() parsing inside a try block in
app/api/earn/layerswap/swap/route.ts lines 17-30, catching malformed JSON and
returning HTTP 400 with { error: "Invalid JSON body" } before destructuring.
Apply the same guard to app/api/earn/layerswap/withdraw-swap/route.ts lines
17-30 before destructuring its withdrawal fields.
- Around line 8-15: Require Privy authentication and resource authorization
across all LayerSwap proxy routes: in app/api/earn/layerswap/swap/route.ts,
authenticate before creating a swap and verify ownership of sourceAddress; in
app/api/earn/layerswap/withdraw-swap/route.ts, authenticate and verify ownership
of the supplied Starknet sourceAddress; in
app/api/earn/layerswap/swap/status/route.ts, authenticate, authorize access to
the requested swap, and return only its status and receive amount.

In `@app/hooks/useEvmEarnHandler.ts`:
- Around line 227-237: Source-position writes overwrite existing balances
instead of accumulating deposits. Add a shared helper such as
addEarnSourcePosition in app/lib/earnPositionStore.ts that reads the current
position, adds deltaBaseUnits, and writes matching base-unit and formatted
totals; update app/hooks/useEvmEarnHandler.ts lines 227-237 and
app/hooks/useEarnBridgeStatusTracker.ts lines 137-157 to use it for their
respective address/source-chain/USDC values.
- Around line 197-225: Update the live bridge flow around executeBatchCalls and
PendingEarnBridgeJob to persist the job before submitting the bridge
transaction, with claimedByLiveFlow set so useEarnBridgeStatusTracker skips it
while the live flow owns recovery. Add the optional claimedByLiveFlow field in
PendingEarnBridgeJob, make the tracker ignore claimed jobs, and clear the claim
when the live flow aborts so recovery remains available. Keep vesuDeposit as the
sole deposit path for the claimed live flow.
- Around line 333-336: Update the withdrawal flow in useEvmEarnHandler around
withdrawToEvm so it no longer calls clearEarnSourcePosition unconditionally
after pollSwapUntilComplete. Use readEarnSourcePosition from earnPositionStore
to load the current USDC position, subtract the withdrawn amountBaseUnits, and
persist the reduced balance instead of deleting the record. Only call
clearEarnSourcePosition when the remaining balance reaches zero, then keep the
existing refreshPosition("USDC") path.
- Around line 165-175: Update both LayerSwap swap-creation POST requests in
useEvmEarnHandler to include the authenticated bearer token in their
Authorization headers. In the corresponding route handlers, validate that bearer
token before accessing or using the server-side LayerSwap API key, preserving
the existing request behavior only after authentication succeeds.

In `@app/lib/config.ts`:
- Around line 87-88: Update the layerswapApiBaseUrl configuration normalization
to remove all trailing slashes from the selected environment value or default,
then validate that the resulting URL uses https:// and reject non-HTTPS values
before requests are constructed. Preserve the existing default while ensuring
downstream /api/v2/... paths do not produce duplicate slashes.

In `@app/lib/layerswapExecute.ts`:
- Around line 109-117: Update the call construction in the action loop so ERC-20
deposits always use native value 0, while actions without a token contract may
attach their parsed native amount. Reuse the guarded amount-parsing behavior
from parseActionTokenAmount for tokenless actions, and avoid converting
amount_in_base_units for actions that carry a token contract.

---

Nitpick comments:
In `@__tests__/layerswapStarknetExecute.test.ts`:
- Around line 3-26: Extend the layerswapDepositActionsToStarknetCalls tests with
cases asserting throws for invalid JSON, missing entrypoint, and call_data equal
to "0x". Add a multiple-action case with out-of-order order values and assert
the resulting calls follow the expected order.

In `@app/components/EarnWalletForm.tsx`:
- Around line 188-206: Update both effects around fetchQuote and
fetchWithdrawQuote so empty or non-positive amountString values invoke the
corresponding fetcher with 0 before returning, clearing stale quote state.
Preserve the existing positive-amount debounce behavior and dependency arrays.

In `@app/hooks/useEarnSourcePosition.ts`:
- Around line 19-21: Remove the localStorage-backed read from the useState
initializer in useEarnSourcePosition, initializing position to null
unconditionally. Keep the existing mount effect responsible for calling
readEarnSourcePosition and hydrating the stored value after client mount.

In `@app/lib/layerswap.ts`:
- Around line 76-99: Update the LayerSwap request error handling around the
quote-fetching method and the other requests using validateStatus: () => true to
preserve and propagate the upstream HTTP status. Include the response status
when throwing for error responses, including bodies without data.error or
non-JSON gateway responses, so callers can distinguish authentication,
rate-limit, server, and unavailable-quote failures instead of mapping them all
to 502.
- Around line 261-270: Update the validation in the loop processing parsed
LayerSwap calls, alongside the existing contractAddress and entrypoint checks,
to require that every c.calldata element is a string. Reject any non-string
calldata entry with the same early validation error before pushing the call,
while preserving the existing default of [] when calldata is absent.

In `@app/providers.tsx`:
- Line 127: Update the provider rendering around EarnBridgeTracker so it mounts
only when config.evmEarnEnabled is true. Preserve the existing tracker behavior
and placement when the feature flag is enabled, and render nothing for the
disabled case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 335bc68f-25ae-4c6e-894f-23d2b98160ba

📥 Commits

Reviewing files that changed from the base of the PR and between 4d3db6b and 9e5c468.

📒 Files selected for processing (31)
  • .env.example
  • __tests__/earnChains.test.ts
  • __tests__/earnFeature.test.ts
  • __tests__/layerswapStarknetExecute.test.ts
  • app/api/earn/layerswap/quote/route.ts
  • app/api/earn/layerswap/starknet-deposit/route.ts
  • app/api/earn/layerswap/swap/route.ts
  • app/api/earn/layerswap/swap/status/route.ts
  • app/api/earn/layerswap/withdraw-quote/route.ts
  • app/api/earn/layerswap/withdraw-swap/route.ts
  • app/components/EarnActivityPanel.tsx
  • app/components/EarnBridgeTracker.tsx
  • app/components/EarnSourcePositionCard.tsx
  • app/components/EarnUnavailableModal.tsx
  • app/components/EarnWalletForm.tsx
  • app/components/MobileDropdown.tsx
  • app/components/WalletDetails.tsx
  • app/components/wallet-mobile-modal/EarnHubView.tsx
  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/hooks/useEarnBridgeStatusTracker.ts
  • app/hooks/useEarnHandler.ts
  • app/hooks/useEarnSourcePosition.ts
  • app/hooks/useEvmEarnHandler.ts
  • app/lib/config.ts
  • app/lib/earnChains.ts
  • app/lib/earnFeature.ts
  • app/lib/earnPositionStore.ts
  • app/lib/layerswap.ts
  • app/lib/layerswapExecute.ts
  • app/providers.tsx
  • app/types.ts

Comment thread .env.example
Comment thread app/api/earn/layerswap/quote/route.ts Outdated
Comment thread app/api/earn/layerswap/starknet-deposit/route.ts Outdated
Comment thread app/api/earn/layerswap/swap/route.ts
Comment thread app/api/earn/layerswap/swap/route.ts Outdated
Comment thread app/hooks/useEvmEarnHandler.ts
Comment thread app/hooks/useEvmEarnHandler.ts Outdated
Comment thread app/hooks/useEvmEarnHandler.ts Outdated
Comment thread app/lib/config.ts Outdated
Comment thread app/lib/layerswapExecute.ts
- Updated .env.example to include optional LayerSwap API base URL configuration.
- Refactored LayerSwap API routes to improve error handling, including timeout responses for transaction confirmations.
- Introduced validation functions for parsing amounts in LayerSwap requests, ensuring robust input handling.
- Enhanced the Earn bridge status tracking to include wallet ID in API calls, improving user-specific data retrieval.
- Updated hooks and utility functions to support new validation and error handling logic, ensuring a smoother user experience.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/lib/earnPositionStore.ts`:
- Around line 193-194: Update the formatting logic for suppliedFormatted at both
affected locations in the earn position update flow to avoid converting bigint
values through Number. Format the USDC base-unit bigint directly by separating
whole units and the six-digit fractional remainder, preserving exact values
above Number.MAX_SAFE_INTEGER while retaining six decimal places.
- Around line 24-35: Update the live-flow claim handling around depositFromEvm
and isStaleLiveFlowClaim to record a claimedAt timestamp when claimedByLiveFlow
is set, and base stale detection on claimedAt when present, falling back to
createdAt for legacy jobs. Add a regression test covering an old createdAt with
a recent claimedAt to ensure the claim is not treated as stale.

In `@app/lib/layerswapRouteAuth.ts`:
- Around line 63-65: Update normalizeStarknetAddress to use the existing
validating, felt-aware Starknet address normalizer before lowercasing, so padded
and unpadded representations canonicalize identically. Ensure both comparison
operands in swapBelongsToUser and assertStarknetAddressOwnedByUser pass through
this shared normalization path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 049c4e4e-4765-4809-b653-d3945da65156

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5c468 and f0b8832.

📒 Files selected for processing (19)
  • .env.example
  • __tests__/earnPositionStore.test.ts
  • __tests__/layerswapExecute.test.ts
  • __tests__/layerswapValidation.test.ts
  • app/api/earn/layerswap/quote/route.ts
  • app/api/earn/layerswap/starknet-deposit/route.ts
  • app/api/earn/layerswap/swap/route.ts
  • app/api/earn/layerswap/swap/status/route.ts
  • app/api/earn/layerswap/withdraw-quote/route.ts
  • app/api/earn/layerswap/withdraw-swap/route.ts
  • app/hooks/useEarnBridgeStatusTracker.ts
  • app/hooks/useEvmEarnHandler.ts
  • app/lib/config.ts
  • app/lib/earnPositionStore.ts
  • app/lib/layerswap.ts
  • app/lib/layerswapConfig.ts
  • app/lib/layerswapExecute.ts
  • app/lib/layerswapRouteAuth.ts
  • app/lib/layerswapValidation.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/api/earn/layerswap/withdraw-quote/route.ts
  • .env.example
  • app/hooks/useEvmEarnHandler.ts
  • app/api/earn/layerswap/starknet-deposit/route.ts
  • app/lib/layerswap.ts
  • app/hooks/useEarnBridgeStatusTracker.ts

Comment thread app/lib/earnPositionStore.ts
Comment thread app/lib/earnPositionStore.ts Outdated
Comment thread app/lib/layerswapRouteAuth.ts Outdated

@Dprof-in-tech Dprof-in-tech left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Dprof-in-tech
Dprof-in-tech merged commit 45f4fd7 into main Aug 6, 2026
3 checks passed
@Dprof-in-tech
Dprof-in-tech deleted the feat/noblocks-earn-evm-phase-2 branch August 6, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants