Skip to content

feat: add private swaps option#10

Merged
GabrielePicco merged 4 commits into
mainfrom
feat/private-swaps
Apr 25, 2026
Merged

feat: add private swaps option#10
GabrielePicco merged 4 commits into
mainfrom
feat/private-swaps

Conversation

@GabrielePicco
Copy link
Copy Markdown
Contributor

@GabrielePicco GabrielePicco commented Apr 25, 2026

Summary by CodeRabbit

  • New Features

    • Added private swap options with configurable transfer delays, split routing, and destination handling (including domain/address resolution).
    • Added mint initialization flow and recipient balance tracking for private transfers.
    • Added UI controls for private routing and URL/state syncing for private swap params.
  • Updates

    • Default token selection swapped: sell SOL, buy USDC.
    • Swap endpoints and error reporting updated for clearer upstream responses; slippage validation now allows zero.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pay Ready Ready Preview, Comment Apr 25, 2026 6:33pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Warning

Rate limit exceeded

@GabrielePicco has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 52 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 52 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 953ba63f-28ec-47a1-b771-6e5fb2b90de9

📥 Commits

Reviewing files that changed from the base of the PR and between f128ebb and 299008d.

📒 Files selected for processing (3)
  • app/api/payments/mint/route.ts
  • app/api/swap/route.ts
  • lib/payments-errors.ts

Walkthrough

Routes for swap operations were migrated from the aggregator API to the payments API; private routing support (visibility, destination, delay range, split) was added across API, hooks, UI, and utilities; default sell/buy token defaults were swapped and Next.js dependency bumped.

Changes

Cohort / File(s) Summary
Payments API Routes & Error Handling
app/api/swap/quote/route.ts, app/api/swap/route.ts, lib/payments.ts, lib/aggregator.ts
Swap quote/build routes now target payments endpoints (PAYMENTS_ENDPOINTS.swapQuote / .../swap) and use payments timeout signals. Aggregator swap endpoints removed. Upstream request construction and error parsing changed to build URLs via getPaymentsApiUrl(...) and parse JSON error bodies.
Private Routing Backend Validation
app/api/swap/route.ts, app/api/swap/quote/route.ts
POST/GET handlers validate visibility (public/private), when private validate destination (Solana PublicKey), minDelayMs/maxDelayMs numeric strings, split integer 1–10, and maxDelayMs >= minDelayMs. Request payloads include private fields when applicable; responses return structured { error, details } JSON parsed from upstream.
Private Routing Utilities
lib/private-routing.ts
New utilities exported: MAX_PRIVATE_DELAY_MS, clampPrivateSplit(), formatPrivateDelayValue(), and formatPrivateRoutingSummary() to clamp split values and format delay/summaries.
UI: Private Routing Controls & PaymentCard
components/one/private-routing-controls.tsx, components/one/payment-card.tsx
Added PrivateRoutingControls component (expandable card, delay slider, split presets/input). PaymentCard refactored to use shared private-routing utilities and the new controls; inline private-transfer UI removed.
SwapCard, TradeHub, and Swap UX
components/one/swap-card.tsx, components/one/trade-hub.tsx
Swap UI/state extended with query params (sprivate, dst, smin, smax, ssplit), destination resolution (address or .sol domain), destination balance subscriptions, mint initialization gating and setup flow, plus private routing UI integration and updated button/disabled states. TradeHub accepts the new swap query params.
Swap Hook / State Management
hooks/use-swap.ts
UseSwapOptions expanded with visibility, destination, destinationPending, minDelayMs, maxDelayMs, split. executeSwap includes private routing fields in the build request and blocks execution when destination is pending/absent for private visibility.
Token Defaults
lib/tokens.ts
Swapped default tokens: DEFAULT_SELL_MINTSOL_MINT, DEFAULT_BUY_MINTUSDC_MINT.
Dependency Bump
package.json
next dependency bumped from 16.1.6 to 16.2.3.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.71% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add private swaps option' directly and clearly summarizes the main objective of the changeset, which adds comprehensive private swap functionality across multiple components and API routes.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/private-swaps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/swap/quote/route.ts`:
- Around line 47-58: The code wraps getPaymentsApiUrl(...) in new URL(...)
causing an unnecessary string->URL conversion; change getPaymentsApiUrl to
return a URL object (or add a new getPaymentsApiUrlAsUrl helper) and update
callers like upstreamUrl in route.ts (where you build the query for
PAYMENTS_ENDPOINTS.swapQuote and call fetch) to use the URL directly (remove new
URL(...) usage) so you avoid the round-trip serialization; ensure all other call
sites of getPaymentsApiUrl are updated to accept/handle a URL return type.

In `@app/api/swap/route.ts`:
- Around line 19-24: The route.ts docblock for the POST body is stale and omits
private-swap fields; update the comment above the Proxy to the payments swap API
(in app/api/swap/route.ts) to include the additional request body fields:
visibility, destination, minDelayMs, maxDelayMs, and split for private mode, and
ensure the return description still mentions privateTransfer as before; locate
the top-of-file JSDoc comment that currently lists quoteResponse, userPublicKey,
dynamicComputeUnitLimit?, prioritizationFeeLamports? and append the new private
mode fields with brief types/optional markers to keep the docs accurate.
- Around line 76-83: The current generic NextResponse.json error for private
swaps hides which field failed; update the validation in the swap route handler
to perform per-field checks for destination presence, numeric parseability of
minDelayMs and maxDelayMs, and split being an integer within 1–10, accumulate a
map/array of field-specific error messages (e.g., errors.destination,
errors.minDelayMs, errors.maxDelayMs, errors.split) and return
NextResponse.json({ errors }, { status: 400 }) when any validation fails so
clients get explicit, field-level error details; adjust the code paths around
the existing NextResponse.json call in this route to use that errors object.
- Around line 115-121: The unreachable fallback for split should be removed:
since earlier validation (hasValidSplit) guarantees split is a number in 1–10
when visibility === "private", set upstreamBody.split directly to split (no ":
1" fallback) inside the block where upstreamBody, visibility, destination,
minDelayMs and maxDelayMs are assigned; update the assignment in that if branch
to remove the dead fallback and rely on the validated value.
- Around line 68-83: The request currently only checks that minDelayMs and
maxDelayMs are numeric strings but allows arbitrarily large values; import
MAX_PRIVATE_DELAY_MS from '@/lib/private-routing' and parse
minDelayMs/maxDelayMs to integers (e.g., const min = parseInt(minDelayMs, 10))
then validate that min and max are within sensible bounds (e.g., min >= 0, max
<= MAX_PRIVATE_DELAY_MS, and min <= max); if a value is out of range return the
same 400 NextResponse with a clear error like "Private swaps require
destination, minDelayMs, maxDelayMs, and split within allowed ranges" so
abusive/malformed delays are rejected early.

In `@components/one/private-routing-controls.tsx`:
- Around line 108-113: The collapse container currently hardcodes "max-h-80"
which can clip taller children; update the div that uses className and the
enabled prop to animate height based on content instead of a fixed max: attach a
ref to the content node (the div with className using enabled), compute its
scrollHeight and set an inline style maxHeight={enabled ? `${scrollHeight}px` :
'0px'} (or animate via CSS grid rows 0fr→1fr using a class toggle) and remove
the hard-coded "max-h-80" class so the panel expands to fit its children (keep
the transition classes for smooth animation).
- Around line 162-174: The number input currently sends raw parsed integers to
onSplitChange (via the split variable and the input's onChange handler),
allowing values outside the intended 1–10 range; update the onChange handler in
private-routing-controls (the input that reads split and calls onSplitChange) to
clamp the parsed value into [1,10] (and still default to 1 on NaN) before
calling onSplitChange—e.g., compute nextValue = Number.parseInt(...,10), if NaN
set 1, then clamp to min 1 and max 10, then call onSplitChange with the clamped
value so the component enforces its contract.

In `@components/one/swap-card.tsx`:
- Around line 600-640: The fetch catch currently resets isMintInitialized to
null causing transient network/parse errors to remove the setup gate; instead
introduce a new state (e.g., mintCheckError) and in the useEffect's catch set
that error (and do not clobber the previous isMintInitialized value), clear it
on successful response, and ensure UI logic that computes requiresMintSetup (and
the setup banner rendering) treats an active mintCheckError as blocking (or
shows an inline error) so users cannot proceed on transient failures; update the
useEffect around fetch(`/api/payments/mint...`) to setMintCheckError(null) on
success, setMintCheckError(err) on catch, and avoid setIsMintInitialized(null)
in the catch branch so previous boolean state is preserved.
- Around line 543-549: Invert the `cancelled` guards to avoid returning inside
the finally block: in the catch block around setRecipientTokenBalance change the
`if (cancelled) return; setRecipientTokenBalance(null);` to only call
setRecipientTokenBalance when not cancelled, and in the finally block remove the
bare `return` and instead conditionally call
setIsRecipientTokenBalanceLoading(false) only when not cancelled (e.g., `if
(!cancelled) { setIsRecipientTokenBalanceLoading(false); }`), ensuring no early
return in finally and preserving existing behavior; look for symbols cancelled,
setRecipientTokenBalance and setIsRecipientTokenBalanceLoading.
- Around line 892-906: handleDelayRangeChange can accept an inverted pair so
enforce max >= min before setting state/URL: in handleDelayRangeChange (and
using MAX_PRIVATE_DELAY_MS) clamp both values to [0, MAX_PRIVATE_DELAY_MS], then
normalize with clampedMax = Math.max(clampedMin, clampedMax) (or swap if needed)
before calling resetSwapIfTerminal, setMinDelayMs, setMaxDelayMs, and
updateSwapUrl so the local state and URL always satisfy the same invariant the
URL parser and app/api/swap/route.ts expect; keep references to
resetSwapIfTerminal, setMinDelayMs, setMaxDelayMs, updateSwapUrl and
MAX_PRIVATE_DELAY_MS.

In `@components/one/trade-hub.tsx`:
- Around line 16-25: The duplicated SWAP_QUERY_PARAMS constant is out of sync
between trade-hub.tsx (SWAP_QUERY_PARAMS with
"buy","sell","amt","sprivate","dst","smin","smax","ssplit") and payment-card.tsx
(only ["buy","sell","amt"]), causing stale swap query params and
hasSwapSelection to misbehave; fix by making them a single source of truth:
either export the full SWAP_QUERY_PARAMS from a shared module and import it into
both trade-hub.tsx and payment-card.tsx, or update payment-card.tsx's
SWAP_QUERY_PARAMS to include "sprivate","dst","smin","smax","ssplit" so the
URL-clearing logic around the payment tab (the effect that strips foreign swap
params) matches trade-hub's constant and prevents stale state.

In `@hooks/use-swap.ts`:
- Around line 182-217: Add a client-side validation in the private-mode gating
before building the request: check visibility === "private" and if
Number(maxDelayMs) < Number(minDelayMs) call setError("Max delay must be >= min
delay") and setStatus("error") and return; place this check alongside the
existing destinationPending/destination checks so it short-circuits before
constructing requestBody or calling fetch, referencing the visibility,
minDelayMs, maxDelayMs, setError, and setStatus symbols.

In `@lib/private-routing.ts`:
- Around line 3-5: clampPrivateSplit currently returns NaN for non-finite
inputs; update the function to coerce the incoming value to a safe finite
integer before clamping: convert value with Number(value), if
Number.isFinite(...) is false default to 1, then truncate/round to an integer
(e.g., Math.trunc or Math.round) and finally clamp between 1 and 10; reference
clampPrivateSplit and ensure callers like swap-card.tsx and
private-routing-controls.tsx continue to receive a guaranteed integer 1–10
string-safe result.
- Around line 36-44: The summary text currently shows "0 ms" when minDelayMs ===
0 and maxDelayMs > 0, which reads awkwardly; update the branch that builds the
range (the final return using minDelayMs/maxDelayMs and formatPrivateDelayValue)
to special-case a zero lower bound: when minDelayMs === 0 render the lower bound
as "Immediate" (or "now") instead of formatPrivateDelayValue(0), e.g.
`${splitLabel} across Immediate-${formatPrivateDelayValue(maxDelayMs)}`, while
keeping existing behavior for equal bounds and the fully-immediate case; use the
same variable names (minDelayMs, maxDelayMs, splitLabel) and the helper
formatPrivateDelayValue to implement this.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: 6a50a01c-aae7-40c6-b9f7-1f50173d21f9

📥 Commits

Reviewing files that changed from the base of the PR and between e5c28f2 and e3849e3.

📒 Files selected for processing (11)
  • app/api/swap/quote/route.ts
  • app/api/swap/route.ts
  • components/one/payment-card.tsx
  • components/one/private-routing-controls.tsx
  • components/one/swap-card.tsx
  • components/one/trade-hub.tsx
  • hooks/use-swap.ts
  • lib/aggregator.ts
  • lib/payments.ts
  • lib/private-routing.ts
  • lib/tokens.ts
💤 Files with no reviewable changes (1)
  • lib/aggregator.ts

Comment thread app/api/swap/quote/route.ts
Comment thread app/api/swap/route.ts
Comment thread app/api/swap/route.ts
Comment thread app/api/swap/route.ts
Comment thread app/api/swap/route.ts
Comment thread components/one/swap-card.tsx
Comment thread components/one/trade-hub.tsx
Comment thread hooks/use-swap.ts
Comment thread lib/private-routing.ts
Comment thread lib/private-routing.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3849e33dd

ℹ️ 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".

Comment thread app/api/swap/quote/route.ts
Comment thread app/api/swap/route.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 947738a8b4

ℹ️ 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".

Comment thread lib/tokens.ts
Comment thread yarn.lock
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/swap/route.ts`:
- Around line 141-158: Extract the shared logic into a single exported helper
named getPaymentsErrorMessage and move it into a new payments helper module;
replace the inline parsing in the swap route handler and the existing mint route
to call getPaymentsErrorMessage(res, responseBody) (or accept status/body) so
both routes parse nested responseBody.error.message, top-level
responseBody.message, and finally fall back to a generic `Payments API error:
<status>` string; update imports in the swap route and mint route to use the new
helper and remove the duplicated inline code.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: 00eb949d-136a-4094-bd2d-70a7331f5dbc

📥 Commits

Reviewing files that changed from the base of the PR and between e3849e3 and f128ebb.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • app/api/swap/route.ts
  • components/one/private-routing-controls.tsx
  • components/one/swap-card.tsx
  • package.json

Comment thread app/api/swap/route.ts
Comment thread components/one/swap-card.tsx
@GabrielePicco GabrielePicco merged commit ba6028c into main Apr 25, 2026
4 checks passed
@GabrielePicco GabrielePicco deleted the feat/private-swaps branch April 25, 2026 22:57
@coderabbitai coderabbitai Bot mentioned this pull request May 22, 2026
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.

1 participant