Skip to content

feat: add stealth address#18

Open
snawaz wants to merge 2 commits into
mainfrom
snawaz/stealth
Open

feat: add stealth address#18
snawaz wants to merge 2 commits into
mainfrom
snawaz/stealth

Conversation

@snawaz

@snawaz snawaz commented May 20, 2026

Copy link
Copy Markdown
Contributor

Create user-handle:

image

Now the destination can be user-handle as well:

image

Summary by CodeRabbit

  • New Features

    • Added stealth handle (.block) support for payment recipients alongside wallet addresses and .sol names
    • Launched Handle management UI for creating and verifying stealth handles
    • Integrated ephemeral RPC for secure signed transaction submission
  • Documentation

    • Updated README with ephemeral RPC environment variable configuration and stealth pool behavior documentation
  • Chores

    • Updated dependencies including axios, protobufjs, hono, ua-parser-js, and ws

@vercel

vercel Bot commented May 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
pay Ready Ready Preview, Comment Jun 19, 2026 12:34pm

Request Review

snawaz commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/one/payment-card.tsx (1)

1339-1361: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Stale dependency: isValidReceiver is in deps but not used in callback body.

The guard at lines 1251-1252 checks isStealthReceiver and resolvedReceiver directly rather than using isValidReceiver. Since isValidReceiver is derived from those values, this is functionally correct but the dependency is misleading. Consider either:

  1. Remove isValidReceiver from deps (it's derived from values already in deps)
  2. Use isValidReceiver in the guard: if (!isValidReceiver) return;

Option 2 is cleaner and more explicit:

Suggested fix
   const handleSend = useCallback(async () => {
     if (!publicKey || !signTransaction || !connected) return;
-    if (isResolvingRecipient) return;
-    if (!isStealthReceiver && !resolvedReceiver) return;
+    if (isResolvingRecipient || !isValidReceiver) return;
     if (!rawAmount || rawAmount === "0") return;
🤖 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 `@components/one/payment-card.tsx` around lines 1339 - 1361, The dependency
list includes isValidReceiver but the effect body checks isStealthReceiver and
resolvedReceiver directly; update the effect callback so it uses isValidReceiver
in the guard (e.g., replace the current checks with if (!isValidReceiver)
return;) and keep isValidReceiver in the dependency array, ensuring the effect's
logic references the derived value rather than the underlying pieces (symbols to
change: isValidReceiver, isStealthReceiver, resolvedReceiver inside the
useEffect callback and the dependency array near the closing bracket).
components/one/trade-hub.tsx (1)

179-230: 🧹 Nitpick | 🔵 Trivial

Align h query param usage with HandleCard behavior
trade-hub.tsx treats the presence of the h query param as a signal to activate the handle tab (hasHandleSelection = Boolean(searchParams.get("h"))), but HandleCard never reads searchParams.get("h") and instead initializes handle from getStoredStealthHandle(owner).
If h is meant for deep-linking a specific handle, parse searchParams.get("h") in HandleCard and use it to pre-populate the input; otherwise remove/rename this “unused” query-param detection to avoid implying the value is consumed.

🤖 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 `@components/one/trade-hub.tsx` around lines 179 - 230, trade-hub.tsx currently
treats the presence of the "h" query param (hasHandleSelection =
Boolean(searchParams.get("h"))) as selecting the handle tab but HandleCard
ignores that param and instead uses getStoredStealthHandle(owner); fix by wiring
the "h" deep-link into HandleCard: read searchParams.get("h") (or accept it as a
prop from trade-hub) and, in HandleCard's initialization logic (where it
currently calls getStoredStealthHandle(owner)), prefer the parsed "h" value to
pre-populate the input/selection (falling back to getStoredStealthHandle(owner)
when "h" is absent); alternatively, if "h" is not intended as a deep-link,
remove/rename the Boolean(searchParams.get("h")) check in trade-hub
(updateTabUrl/hasHandleSelection) to stop implying consumption — reference
symbols: hasHandleSelection, searchParams.get("h"), HandleCard,
getStoredStealthHandle(owner), and updateTabUrl.
🤖 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/api/payments/send/route.ts`:
- Around line 78-89: Wrap the call to request.json() in a try/catch and return
an HTTP 400 response for malformed JSON instead of letting it bubble to the
generic 502; specifically, around the code that assigns body and destructures
signedTransaction, blockhash, lastValidBlockHeight, sendTo (and the similar
block later around the other request.json() usage), catch a SyntaxError (or any
parse error) and return a Response with status 400 and a short error message
indicating invalid JSON payload so clients receive a proper client error.

In `@app/api/payments/stealth-pool/route.ts`:
- Around line 76-82: The auth header check using authHeader?.startsWith("Bearer
") is case-sensitive and should accept any casing of the scheme; update the
logic in route.ts where authHeader is read (variable authHeader) and the
startsWith check to perform a case-insensitive match (e.g., test the scheme
portion with toLowerCase() === "bearer" or use a case-insensitive regex like
/^bearer\s+/i), then extract the token after the scheme safely (split on
whitespace and use the second element) and return the same 401 response if the
scheme is not "bearer" or the token is missing.

In `@lib/payment-transactions.ts`:
- Around line 138-147: The POST fetches to "/api/payments/send" (the call that
sends serializeSignedPaymentTransaction(signedTransaction) and
unsignedTransaction fields) need a client-side timeout to avoid hanging; wrap
each fetch in an AbortController with a setTimeout that calls controller.abort()
after a configurable timeout (e.g. 10s), pass controller.signal to fetch, and
clear the timeout on success; also handle the abort by catching the thrown
DOMException/AbortError and surface a clear error (or return a timeout-specific
error) so callers of this payment flow can recover.

In `@lib/stealth-handles.ts`:
- Around line 25-27: The setter setStoredStealthHandle currently writes directly
to localStorage and can throw in restricted environments; update it to mirror
the getter’s safety guards by checking for a window/localStorage environment
(e.g., typeof window !== 'undefined' and window.localStorage) and wrapping the
write in a try/catch, using STORAGE_PREFIX to build the key and swallowing or
logging errors instead of allowing exceptions to propagate from
localStorage.setItem.

In `@README.md`:
- Line 34: Update the README line describing `PAYMENTS_EPHEMERAL_RPC_URL` and
`EPHEMERAL_RPC_URL` to expand the "ER" acronym for clarity—replace or augment
"ER" with "Ephemeral RPC" (or "Ephemeral RPC (ER)") so the sentence reads
something like: "`PAYMENTS_EPHEMERAL_RPC_URL` or `EPHEMERAL_RPC_URL`: ephemeral
RPC (ER) used when signed transactions must be submitted to Ephemeral RPC."
Reference the environment variable names `PAYMENTS_EPHEMERAL_RPC_URL` and
`EPHEMERAL_RPC_URL` when making the change.
- Line 34: Update the README description for the PAYMENTS_EPHEMERAL_RPC_URL and
EPHEMERAL_RPC_URL entries to state that Bearer authentication is required when
submitting signed transactions to the ephemeral RPC; explicitly note that
requests must include an Authorization: Bearer <token> header (or equivalent)
and mention that users must obtain/configure a valid token when setting those
environment variables so the endpoint will accept submissions.
- Around line 51-52: Remove the unintended trailing blank line after the
sentence describing the Handle tab so the paragraph ends immediately after
"private stealth-transfer route."; locate the sentence containing "Handle tab"
and ".block" and delete the empty line following it to clean up the README
formatting.

---

Outside diff comments:
In `@components/one/payment-card.tsx`:
- Around line 1339-1361: The dependency list includes isValidReceiver but the
effect body checks isStealthReceiver and resolvedReceiver directly; update the
effect callback so it uses isValidReceiver in the guard (e.g., replace the
current checks with if (!isValidReceiver) return;) and keep isValidReceiver in
the dependency array, ensuring the effect's logic references the derived value
rather than the underlying pieces (symbols to change: isValidReceiver,
isStealthReceiver, resolvedReceiver inside the useEffect callback and the
dependency array near the closing bracket).

In `@components/one/trade-hub.tsx`:
- Around line 179-230: trade-hub.tsx currently treats the presence of the "h"
query param (hasHandleSelection = Boolean(searchParams.get("h"))) as selecting
the handle tab but HandleCard ignores that param and instead uses
getStoredStealthHandle(owner); fix by wiring the "h" deep-link into HandleCard:
read searchParams.get("h") (or accept it as a prop from trade-hub) and, in
HandleCard's initialization logic (where it currently calls
getStoredStealthHandle(owner)), prefer the parsed "h" value to pre-populate the
input/selection (falling back to getStoredStealthHandle(owner) when "h" is
absent); alternatively, if "h" is not intended as a deep-link, remove/rename the
Boolean(searchParams.get("h")) check in trade-hub
(updateTabUrl/hasHandleSelection) to stop implying consumption — reference
symbols: hasHandleSelection, searchParams.get("h"), HandleCard,
getStoredStealthHandle(owner), and updateTabUrl.
🪄 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: 7c3a5f0a-cd29-4850-95db-292ec60713a7

📥 Commits

Reviewing files that changed from the base of the PR and between e967b83 and ee32852.

📒 Files selected for processing (13)
  • README.md
  • app/api/payments/send/route.ts
  • app/api/payments/stealth-pool/route.ts
  • app/api/payments/transfer-queue/ensure-crank/route.ts
  • app/api/payments/transfer-stealth/route.ts
  • components/one/handle-card.tsx
  • components/one/payment-card.tsx
  • components/one/swap-card.tsx
  • components/one/trade-hub.tsx
  • lib/payment-transactions.ts
  • lib/payments.ts
  • lib/solana-rpc.ts
  • lib/stealth-handles.ts

Comment thread app/api/payments/send/route.ts
Comment thread app/api/payments/stealth-pool/route.ts
Comment thread lib/payment-transactions.ts
Comment thread lib/stealth-handles.ts
Comment thread README.md
Comment thread README.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/one/handle-card.tsx (1)

304-331: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Retry after partial success may cause duplicate or wasted transactions.

If ensureTransaction succeeds (line 305-308) but updateTransaction fails with blockhash expiration, the retry loop rebuilds and resubmits both transactions. This could:

  1. Submit a duplicate ensure transaction (wasting fees if idempotent, or causing errors if not)
  2. Leave the user confused about which transaction to track

Consider tracking ensure success within the loop and skipping its resubmission on retry:

Proposed fix
       let body: StealthPoolBuildResponse | null = null;
       let nextSignature = "";
+      let ensureAlreadySubmitted = false;

       for (let attempt = 0; attempt < 2; attempt += 1) {
         try {
           // ... building and signing ...

           setStatus("sending");
-          const ensureSignature = await submitSignedPaymentTransaction(
-            ensureTransaction,
-            signedSetupTransaction
-          );
-          setSignature(ensureSignature);
+          if (!ensureAlreadySubmitted) {
+            const ensureSignature = await submitSignedPaymentTransaction(
+              ensureTransaction,
+              signedSetupTransaction
+            );
+            setSignature(ensureSignature);
+            ensureAlreadySubmitted = true;
+          }
           nextSignature = await submitSignedPaymentTransaction(
             updateTransaction,
             signedUpdatePoolTransaction,
             authToken
           );
           // ...
         } catch (err) {
           // ...
           if (attempt === 0 && isPaymentBlockhashExpiredError(err)) {
-            setSignature(null);
+            // Keep signature if ensure already submitted
+            if (!ensureAlreadySubmitted) {
+              setSignature(null);
+            }
             continue;
           }
           throw err;
         }
       }
🤖 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 `@components/one/handle-card.tsx` around lines 304 - 331, The retry loop may
resubmit both ensureTransaction and updateTransaction when only
updateTransaction fails with blockhash expiration, potentially wasting fees on
duplicate ensure transaction submissions. Track whether the ensureTransaction
was successfully submitted (when setSignature is called) using a flag at the
beginning of the while loop, and on retry, skip the ensureTransaction submission
if it already succeeded in a previous attempt. Only resubmit updateTransaction
on the blockhash expiration retry.
🤖 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.

Outside diff comments:
In `@components/one/handle-card.tsx`:
- Around line 304-331: The retry loop may resubmit both ensureTransaction and
updateTransaction when only updateTransaction fails with blockhash expiration,
potentially wasting fees on duplicate ensure transaction submissions. Track
whether the ensureTransaction was successfully submitted (when setSignature is
called) using a flag at the beginning of the while loop, and on retry, skip the
ensureTransaction submission if it already succeeded in a previous attempt. Only
resubmit updateTransaction on the blockhash expiration retry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: be50aba5-769b-4018-bbe4-1edbdd1a78ec

📥 Commits

Reviewing files that changed from the base of the PR and between ee32852 and b02a4f1.

📒 Files selected for processing (2)
  • components/one/handle-card.tsx
  • lib/stealth-handles.ts

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds .block stealth handle pools for private Solana payments. New API routes handle transaction submission, stealth pool creation/lookup, and transfer-queue cranking. A shared lib/payment-transactions.ts module centralizes transaction serialization and submission. A new HandleCard component provides stealth pool creation UI. PaymentCard gains stealth receiver detection, private routing enforcement, and polling-based confirmation.

Changes

Stealth Handles and Private Payments

Layer / File(s) Summary
Shared primitives: stealth handles, payment transactions, endpoints, ephemeral RPC
lib/stealth-handles.ts, lib/payment-transactions.ts, lib/payments.ts, lib/solana-rpc.ts, README.md
Defines stealth-handle constants, validation, and localStorage helpers; adds UnsignedPaymentTransaction, PaymentTransactionSubmissionError, serialization functions, and network helpers (submitSignedPaymentTransaction, ensurePaymentTransferQueueCrank); registers two new PAYMENTS_ENDPOINTS entries; adds createPaymentsEphemeralConnection with env-var fallback; documents PAYMENTS_EPHEMERAL_RPC_URL and Handle-tab behavior.
POST /api/payments/send: transaction submission with ephemeral/base routing
app/api/payments/send/route.ts
New route decodes a base64 signed transaction, enforces Bearer auth for ephemeral mode, checks fee-payer funding for base mode, submits via the selected connection with mode-dependent preflight, confirms on-chain, and returns structured errors with extracted logs or a 502 fallback.
GET+POST /api/payments/stealth-pool: pool check and creation
app/api/payments/stealth-pool/route.ts
GET validates a stealth handle and proxies pool-existence checks upstream. POST enforces Bearer auth, validates payer/authority/destinations as Solana PublicKeys with length constraints, and forwards pool-build requests upstream with the auth header.
POST /api/payments/transfer-queue/ensure-crank: transfer queue maintenance
app/api/payments/transfer-queue/ensure-crank/route.ts
Validates mint and optional validator as Solana PublicKeys, forwards to the upstream crank endpoint with a 30 s timeout, and returns upstream JSON or a normalized error response.
Stealth handle support in POST /api/payments/transfer
app/api/payments/transfer/route.ts
Normalizes to via getExactStealthHandleInput, skips PublicKey parsing for stealth recipients, enforces private+base-to-base routing for stealth handles, computes resolved visibility/balance defaults, and adjusts the upstream payload accordingly.
HandleCard component: stealth pool creation UI and transaction flow
components/one/handle-card.tsx
New HandleCard manages handle and destination key inputs with validation, checks pool existence via GET, authenticates via SPL challenge, POSTs build parameters, deserializes and signs the ensure and update transactions in sequence, retries on expired blockhash, and shows confirmation with explorer links.
PaymentCard: stealth receiver routing, transaction utility migration, and UI updates
components/one/payment-card.tsx
Adds stealth handle detection to receiver parsing, auto-enables private routing for stealth receivers, adjusts transfer payloads for stealth, replaces file-local deserialization with shared helpers, switches confirmation to polling, adds post-mint-setup crank call, and updates placeholder text, disabled labels, and error text wrapping.
TradeHub Handle tab routing and SwapCard crank integration
components/one/trade-hub.tsx, components/one/swap-card.tsx
Adds a Handle tab with AtSign icon and ?h= URL parameter routing to TradeHub, renders HandleCard when active, and adds a best-effort ensurePaymentTransferQueueCrank call in SwapCard post-mint-setup.
Dependency version pins and resolutions
package.json
Pins @sentry/nextjs to 10.54.0 and expands resolutions to enforce specific versions of axios, protobufjs, hono, @opentelemetry/*, @trezor/env-utils/ua-parser-js, and ws.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • magicblock-labs/one#10: Introduced PrivateRoutingControls and private-routing UI logic in payment-card.tsx; this PR extends that component with stealth-receiver overrides for the same controls.
  • magicblock-labs/one#19: Modifies app/api/payments/transfer/route.ts and components/one/payment-card.tsx on the same shielded/ephemeral routing and auth-token validation code paths touched by this PR.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: add stealth address' is vague and generic. While it references a real feature added in the PR, it does not convey the scope or specificity of the extensive changes (new APIs, UI components, transaction submission flows, handle management, etc.). The title lacks sufficient detail about what 'stealth address' entails in this context. Consider expanding the title to clarify the scope of changes, such as 'feat: add stealth handle creation with transaction submission' or 'feat: implement stealth pool and handle UI flow' to better represent the substantive changes included.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch snawaz/stealth

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/one/trade-hub.tsx (1)

118-129: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make ?h= both win tab selection and prefill the handle.

hasHandleSelection is checked after payment/request/shield params, and <HandleCard /> receives no h value. A URL like ?h=alice.block&rcv=... opens Payment, while ?h=alice.block opens Handle but shows the stored/blank handle instead of the URL handle.

Proposed direction
   const hasHandleSelection = Boolean(searchParams.get("h"));
+  const initialHandle = searchParams.get("h") ?? "";
   const [activeTop, setActiveTop] = useState<TopTab>(
     selectableUrlTab
       ? selectableUrlTab
+      : hasHandleSelection
+        ? "handle"
       : hasPaymentSelection
         ? "payment"
@@
-            : hasHandleSelection
-              ? "handle"
-              : hasSwapSelection && !isSwapDisabled
+            : hasSwapSelection && !isSwapDisabled
@@
-    if (hasPaymentSelection) {
+    if (hasHandleSelection) {
+      setActiveTop("handle");
+      return;
+    }
+
+    if (hasPaymentSelection) {
       setActiveTop("payment");
       return;
     }
@@
-    if (hasHandleSelection) {
-      setActiveTop("handle");
-      return;
-    }
-
@@
-      {activeTop === "handle" && <HandleCard />}
+      {activeTop === "handle" && <HandleCard initialHandle={initialHandle} />}

This also requires adding an initialHandle prop in components/one/handle-card.tsx and using it when the wallet owner initializes the local handle state.

Also applies to: 143-160, 315-315

🤖 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 `@components/one/trade-hub.tsx` around lines 118 - 129, The handle parameter
(?h=) doesn't properly win tab selection and doesn't prefill the handle input
because hasHandleSelection is checked last in the conditional chain, allowing
other parameters to take precedence. Fix this by reordering the tab selection
logic in the useState initialization so hasHandleSelection is checked earlier
with higher priority than the payment, request, and shield checks. Additionally,
extract the handle value from searchParams.get("h") and pass it as an
initialHandle prop to the HandleCard component. Then add an initialHandle prop
to the HandleCard component definition in handle-card.tsx and use this value
when initializing the local handle state.
🤖 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/api/payments/stealth-pool/route.ts`:
- Line 84: Wrap the `request.json()` call in a try-catch block to handle JSON
parsing errors separately. When `request.json()` fails to parse the incoming
payload (due to malformed JSON), catch that error and return a 400 Bad Request
response with an appropriate error message instead of allowing it to be caught
by the generic error handler. This ensures client input errors are properly
classified as 400 errors rather than 500 server errors.

In `@components/one/handle-card.tsx`:
- Around line 414-423: The input elements for handle and owner-key are missing
accessible names, making them inaccessible to screen reader users. Add
aria-label attributes to both input elements to provide descriptive accessible
names. For the handle input (the one with placeholder "e.g satoshi.block,
nakamoto.block, etc"), add an appropriate aria-label describing what should be
entered. Similarly, add an aria-label to the owner-key input mentioned at lines
473-480. Choose descriptive labels that clearly indicate the purpose of each
input field.
- Around line 276-279: The build request error handling in the code snippet
starting at line 276 throws a generic Error when res.ok is false, but does not
clear stale auth tokens when the response status is 401. The 401 token cleanup
logic exists elsewhere but only applies to transaction-submission errors. Before
throwing the Error in the !res.ok condition, check if res.status is 401 and if
so, perform the same auth token cleanup that occurs at line 319 for
transaction-submission failures (such as removing the stored token from
localStorage). This ensures stale tokens are cleared regardless of whether the
401 comes from the build request or transaction submission.
- Around line 579-582: The disabled condition for the button in the disabled
prop only checks for !signMessage, but the saveHandle function returns early
when !signTransaction, causing wallets without transaction signing capability to
display an enabled button that cannot function. Add !signTransaction to the
existing disabled condition alongside the other checks (isBusy, isValidHandle,
hasValidDestinations, and signMessage) to ensure the button is properly disabled
when the wallet cannot sign transactions.

In `@components/one/payment-card.tsx`:
- Around line 752-755: The useEffect hook currently only sets isPrivate to true
when a stealth receiver is detected, but handleSend always sends fromBalance:
"base" and toBalance: "base" for stealth receivers, creating a UI mismatch. When
isStealthReceiver becomes true and isPrivate is false, in addition to setting
isPrivate to true, also reset the routing delays to 0 and ensure that the
fromBalance and toBalance states are set to "base" to align the UI state with
what will actually be submitted in the payload. This prevents the UI from
showing a different route than what gets sent.
- Around line 39-44: The imported type UnsignedPaymentTransaction from
`@/lib/payment-transactions` collides with a local interface declaration with the
same name around lines 90-102, causing a duplicate declaration error. Alias the
imported UnsignedPaymentTransaction type to a different name in the import
statement (such as SharedUnsignedPaymentTransaction), then update the local
interface to extend this aliased type while adding the component-specific fields
from and sendRpcEndpoint.

---

Outside diff comments:
In `@components/one/trade-hub.tsx`:
- Around line 118-129: The handle parameter (?h=) doesn't properly win tab
selection and doesn't prefill the handle input because hasHandleSelection is
checked last in the conditional chain, allowing other parameters to take
precedence. Fix this by reordering the tab selection logic in the useState
initialization so hasHandleSelection is checked earlier with higher priority
than the payment, request, and shield checks. Additionally, extract the handle
value from searchParams.get("h") and pass it as an initialHandle prop to the
HandleCard component. Then add an initialHandle prop to the HandleCard component
definition in handle-card.tsx and use this value when initializing the local
handle state.
🪄 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: be347808-044a-402a-944f-402c6d53eac9

📥 Commits

Reviewing files that changed from the base of the PR and between a3f8a82 and 4ed1cce.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (14)
  • README.md
  • app/api/payments/send/route.ts
  • app/api/payments/stealth-pool/route.ts
  • app/api/payments/transfer-queue/ensure-crank/route.ts
  • app/api/payments/transfer/route.ts
  • components/one/handle-card.tsx
  • components/one/payment-card.tsx
  • components/one/swap-card.tsx
  • components/one/trade-hub.tsx
  • lib/payment-transactions.ts
  • lib/payments.ts
  • lib/solana-rpc.ts
  • lib/stealth-handles.ts
  • package.json

);
}

const body = (await request.json()) as StealthPoolBuildRequest;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return 400 for malformed JSON payloads in POST.

request.json() parse errors currently fall through to the generic 500 catch, so client input errors are misclassified as server failures.

Suggested fix
-    const body = (await request.json()) as StealthPoolBuildRequest;
+    let body: StealthPoolBuildRequest;
+    try {
+      body = (await request.json()) as StealthPoolBuildRequest;
+    } catch {
+      return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const body = (await request.json()) as StealthPoolBuildRequest;
let body: StealthPoolBuildRequest;
try {
body = (await request.json()) as StealthPoolBuildRequest;
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
🤖 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/api/payments/stealth-pool/route.ts` at line 84, Wrap the `request.json()`
call in a try-catch block to handle JSON parsing errors separately. When
`request.json()` fails to parse the incoming payload (due to malformed JSON),
catch that error and return a 400 Bad Request response with an appropriate error
message instead of allowing it to be caught by the generic error handler. This
ensures client input errors are properly classified as 400 errors rather than
500 server errors.

Comment on lines +276 to +279
const responseBody = await res.json().catch(() => null);
if (!res.ok) {
throw new Error(responseBody?.error || `Build failed: ${res.status}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale auth tokens when the build request returns 401.

Line 277 throws a plain Error for /api/payments/stealth-pool failures, so the 401 cleanup at Line 319 only runs for transaction-submission errors. A stale stored token will keep being reused and the user cannot save until localStorage is manually cleared.

Proposed fix
           const responseBody = await res.json().catch(() => null);
           if (!res.ok) {
+            if (res.status === 401) {
+              clearStoredPrivateAuthToken(owner);
+              if (attempt === 0) {
+                setSignature(null);
+                continue;
+              }
+            }
             throw new Error(responseBody?.error || `Build failed: ${res.status}`);
           }

Also applies to: 317-323

🤖 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 `@components/one/handle-card.tsx` around lines 276 - 279, The build request
error handling in the code snippet starting at line 276 throws a generic Error
when res.ok is false, but does not clear stale auth tokens when the response
status is 401. The 401 token cleanup logic exists elsewhere but only applies to
transaction-submission errors. Before throwing the Error in the !res.ok
condition, check if res.status is 401 and if so, perform the same auth token
cleanup that occurs at line 319 for transaction-submission failures (such as
removing the stored token from localStorage). This ensures stale tokens are
cleared regardless of whether the 401 comes from the build request or
transaction submission.

Comment on lines +414 to +423
<input
type="text"
value={handle}
onChange={(event) => {
setHandle(event.target.value);
resetResultState();
}}
placeholder="e.g satoshi.block, nakamoto.block, etc"
className="w-full bg-transparent font-mono text-lg text-foreground placeholder:text-muted-foreground/40 outline-none"
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add accessible names to the handle and owner-key inputs.

These inputs are the primary controls for this flow but are not associated with labels, so screen-reader users cannot tell what to enter.

Proposed fix
             <input
               type="text"
+              aria-label="Stealth handle"
               value={handle}
               onChange={(event) => {
                 setHandle(event.target.value);
                 resetResultState();
               }}
@@
                     <input
                       type="text"
+                      aria-label={`Backing owner key ${index + 1}`}
                       value={destination}
                       onChange={(event) =>
                         updateDestination(index, event.target.value)
                       }

Also applies to: 473-480

🧰 Tools
🪛 React Doctor (0.5.6)

[warning] 414-414: Blind users can't tell what this control does because screen readers find no label, so add visible text, aria-label, or aria-labelledby.

Give every interactive control a label screen readers can read.

(control-has-associated-label)


[warning] 414-414: This JSX crashes because React isn't in scope.

If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import React at the top of the file.

(react-in-jsx-scope)

🤖 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 `@components/one/handle-card.tsx` around lines 414 - 423, The input elements
for handle and owner-key are missing accessible names, making them inaccessible
to screen reader users. Add aria-label attributes to both input elements to
provide descriptive accessible names. For the handle input (the one with
placeholder "e.g satoshi.block, nakamoto.block, etc"), add an appropriate
aria-label describing what should be entered. Similarly, add an aria-label to
the owner-key input mentioned at lines 473-480. Choose descriptive labels that
clearly indicate the purpose of each input field.

Source: Linters/SAST tools

Comment on lines +579 to +582
disabled={
connected &&
(isBusy || !isValidHandle || !hasValidDestinations || !signMessage)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disable Save when the wallet cannot sign transactions.

saveHandle returns early when !signTransaction, but the button only disables for !signMessage; wallets without transaction signing will see an enabled button that does nothing.

Proposed fix
             disabled={
               connected &&
-              (isBusy || !isValidHandle || !hasValidDestinations || !signMessage)
+              (isBusy ||
+                !isValidHandle ||
+                !hasValidDestinations ||
+                !signMessage ||
+                !signTransaction)
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
disabled={
connected &&
(isBusy || !isValidHandle || !hasValidDestinations || !signMessage)
}
disabled={
connected &&
(isBusy ||
!isValidHandle ||
!hasValidDestinations ||
!signMessage ||
!signTransaction)
}
🤖 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 `@components/one/handle-card.tsx` around lines 579 - 582, The disabled
condition for the button in the disabled prop only checks for !signMessage, but
the saveHandle function returns early when !signTransaction, causing wallets
without transaction signing capability to display an enabled button that cannot
function. Add !signTransaction to the existing disabled condition alongside the
other checks (isBusy, isValidHandle, hasValidDestinations, and signMessage) to
ensure the button is properly disabled when the wallet cannot sign transactions.

Comment on lines +39 to +44
import {
PaymentTransactionSubmissionError,
type UnsignedPaymentTransaction,
deserializeUnsignedPaymentTransaction,
ensurePaymentTransferQueueCrank,
} from "@/lib/payment-transactions";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Show the imported and local UnsignedPaymentTransaction declarations in this module.
sed -n '35,105p' components/one/payment-card.tsx

Repository: magicblock-labs/one

Length of output: 1985


Resolve the UnsignedPaymentTransaction type collision.

This module imports type UnsignedPaymentTransaction from @/lib/payment-transactions (lines 40-44) and declares a local interface with the same name (lines 90-102), which TypeScript rejects as a duplicate declaration. Alias the shared type and extend it locally for the additional fields used here (from and sendRpcEndpoint).

Proposed fix
 import {
   PaymentTransactionSubmissionError,
-  type UnsignedPaymentTransaction,
+  type UnsignedPaymentTransaction as BaseUnsignedPaymentTransaction,
   deserializeUnsignedPaymentTransaction,
   ensurePaymentTransferQueueCrank,
 } from "`@/lib/payment-transactions`";
@@
-interface UnsignedPaymentTransaction {
-  kind: string;
-  version?: "legacy" | "v0" | 0 | "0";
-  transactionBase64: string;
-  sendTo: "base" | "ephemeral";
+interface UnsignedPaymentTransaction extends BaseUnsignedPaymentTransaction {
   from?: "base" | "ephemeral";
-  recentBlockhash: string;
-  lastValidBlockHeight: number;
-  instructionCount: number;
-  requiredSigners: string[];
-  validator?: string;
   sendRpcEndpoint?: string;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {
PaymentTransactionSubmissionError,
type UnsignedPaymentTransaction,
deserializeUnsignedPaymentTransaction,
ensurePaymentTransferQueueCrank,
} from "@/lib/payment-transactions";
import {
PaymentTransactionSubmissionError,
type UnsignedPaymentTransaction as BaseUnsignedPaymentTransaction,
deserializeUnsignedPaymentTransaction,
ensurePaymentTransferQueueCrank,
} from "`@/lib/payment-transactions`";
🤖 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 `@components/one/payment-card.tsx` around lines 39 - 44, The imported type
UnsignedPaymentTransaction from `@/lib/payment-transactions` collides with a local
interface declaration with the same name around lines 90-102, causing a
duplicate declaration error. Alias the imported UnsignedPaymentTransaction type
to a different name in the import statement (such as
SharedUnsignedPaymentTransaction), then update the local interface to extend
this aliased type while adding the component-specific fields from and
sendRpcEndpoint.

Comment on lines +752 to +755
useEffect(() => {
if (!isStealthReceiver || isPrivate) return;
setIsPrivate(true);
}, [isStealthReceiver, isPrivate]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep stealth UI state aligned with the base/base payload.

For stealth receivers, handleSend always sends fromBalance: "base" and toBalance: "base", but the effect only flips isPrivate. If the user previously selected shielded source/destination, the UI can show one route while the submitted payload uses another; entering a stealth handle from public mode also leaves routing delays at 0.

Proposed fix
   useEffect(() => {
-    if (!isStealthReceiver || isPrivate) return;
-    setIsPrivate(true);
-  }, [isStealthReceiver, isPrivate]);
+    if (!isStealthReceiver) return;
+    if (!isPrivate) setIsPrivate(true);
+    if (sourceBalance !== "base") setSourceBalance("base");
+    if (recipientBalance !== "base") setRecipientBalance("base");
+    if (minDelayMs === 0 && maxDelayMs === 0) {
+      setMinDelayMs(DEFAULT_MIN_DELAY_MS);
+      setMaxDelayMs(DEFAULT_MAX_DELAY_MS);
+    }
+  }, [
+    isStealthReceiver,
+    isPrivate,
+    sourceBalance,
+    recipientBalance,
+    minDelayMs,
+    maxDelayMs,
+  ]);

Also applies to: 1748-1749

🤖 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 `@components/one/payment-card.tsx` around lines 752 - 755, The useEffect hook
currently only sets isPrivate to true when a stealth receiver is detected, but
handleSend always sends fromBalance: "base" and toBalance: "base" for stealth
receivers, creating a UI mismatch. When isStealthReceiver becomes true and
isPrivate is false, in addition to setting isPrivate to true, also reset the
routing delays to 0 and ensure that the fromBalance and toBalance states are set
to "base" to align the UI state with what will actually be submitted in the
payload. This prevents the UI from showing a different route than what gets
sent.

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