Skip to content

Conversation

@Dprof-in-tech
Copy link
Collaborator

@Dprof-in-tech Dprof-in-tech commented Jan 8, 2026

Description

This pull request introduces comprehensive Starknet Layer 2 support to the application, enabling wallet creation, public key retrieval, token transfers, and order creation on Starknet via new API routes. It also adds associated environment configuration for Starknet and refactors wallet address handling in the frontend to improve maintainability and reliability. Additionally, a minor fix expands wallet address validation to support 64-character hexadecimal addresses.

The most important changes are:

Starknet Integration:

Frontend Refactor:

  • Refactored wallet address handling in MainPageContent.tsx to use the new useWalletAddress hook, improving code clarity and maintainability. [1] [2]
  • Minor code formatting and cleanup in imports and function definitions for better readability. [1] [2]

Validation Improvements:

  • Expanded wallet address validation in track-logout/route.ts to support both 40- and 64-character hexadecimal addresses.

References

closes #321

Testing

see loom here Loom

  • This change adds test coverage for new/changed/fixed functionality

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

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

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Starknet network support, enabling users to create wallets, view balances, and transfer tokens on Starknet.
    • Implemented network switching functionality to seamlessly transition between blockchain networks.
  • Improvements

    • Enhanced wallet address validation to support additional address formats.
    • Updated dependencies to improve compatibility and performance.

✏️ Tip: You can customize this high-level summary in your review settings.

@Dprof-in-tech Dprof-in-tech requested a review from chibie as a code owner January 8, 2026 12:24
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

This PR adds comprehensive Starknet network support via Privy's Tier 2 chain integration. It introduces Starknet wallet creation/management, API routes for orders and transfers, balance fetching, and integrates Starknet throughout network selection, transfer workflows, and UI components. Core dependencies updated to enable Starknet signing and RPC interactions.

Changes

Cohort / File(s) Summary
Environment & Configuration
.env.example, package.json
Added 7 Starknet-specific environment variables (RPC URL, class hash, paymaster config). Upgraded @privy-io/server-auth to ^1.32.2 and added starknet ^9.2.1 dependency.
Starknet Context & State Management
app/context/StarknetContext.tsx, app/context/BalanceContext.tsx, app/context/TokensContext.tsx, app/context/index.ts
New StarknetProvider with wallet state, localStorage sync, and wallet creation flow. BalanceContext extended to include Starknet-specific balance fetching and display. TokensContext updated to skip Starknet during fallback token merge. Context exports added to public API.
Starknet API Routes
app/api/starknet/create-wallet/route.ts, app/api/starknet/get-public-key/route.ts, app/api/starknet/create-order/route.ts, app/api/starknet/transfer/route.ts
Four new API endpoints: wallet creation via Privy, public key retrieval, order creation with paymaster support, and token transfers. All routes include JWT authentication, validation, paymaster configuration, and fee estimation.
Track Logout Update
app/api/track-logout/route.ts
Extended wallet address regex to accept both 40-char (EVM) and 64-char (Starknet) hex formats.
Starknet Core Libraries
app/lib/starknet.ts, app/lib/authorization.ts, app/lib/privy.ts, app/utils.ts
New starknet.ts provides Ready account utilities (address computation, signing via Privy, account building, deployment, paymaster setup). New authorization.ts adds cached authorization key retrieval and signature building. Updated privy.ts to add client caching and TEE wallet auth support. Extended utils.ts with Starknet balance fetching, address normalization, gateway contract, explorer/RPC URLs, and network switching with wallet creation.
Network & Token Data
app/mocks.ts
Added starknetMainnet chain definition with network metadata and integrated into exported networks list.
Wallet Address Resolution
app/hooks/useWalletAddress.ts
New hook that selects appropriate wallet address based on context: injected wallet → injectedAddress; Starknet network → Starknet address; otherwise → smart wallet address.
Transfer & Order Creation
app/hooks/useSmartWalletTransfer.ts, app/pages/TransactionPreview.tsx
useSmartWalletTransfer extended with optional starknetWallet parameter and routing to /api/starknet/transfer when target is Starknet. TransactionPreview enhanced to call new /api/starknet/create-order endpoint for Starknet order creation with full wallet/paymaster flow.
Component Integration - Address & Balance
app/components/MainPageContent.tsx, app/components/Navbar.tsx, app/components/WalletDetails.tsx, app/components/SettingsDropdown.tsx, app/pages/TransactionForm.tsx
Unified address retrieval via useWalletAddress hook across components. Balance display extended to use Starknet-specific balances when network is Starknet. SettingsDropdown adds cleanup of 4 Starknet localStorage keys on logout with error recovery.
Component Integration - Network Selection
app/components/MobileDropdown.tsx, app/components/NetworkSelectionModal.tsx, app/components/NetworksDropdown.tsx
Updated network switch flows to optionally call ensureWalletExists for Starknet wallet auto-creation when switching networks.
Component Integration - Transfer Form
app/components/TransferForm.tsx
Extended to retrieve Starknet wallet details via useStarknet, fetch Starknet-specific balances, validate both EVM (42-char) and Starknet (66-char) recipient addresses, and pass starknetWallet to transfer hook.
CNGN Rate Handling
app/hooks/useCNGNRate.ts
Added early-exit guards to skip rate fetching when network name includes "starknet".
Provider Wrapping
app/providers.tsx
Added StarknetProvider to wrap the existing provider tree, enabling Starknet context availability throughout the app.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App
    participant Backend
    participant Privy
    participant StarknetRPC as Starknet RPC

    User->>App: Request Starknet wallet creation
    App->>App: Retrieve JWT token via getAccessToken
    App->>Backend: POST /api/starknet/create-wallet (JWT)
    Backend->>Privy: Verify JWT & extract userId
    Backend->>Privy: Check user's linked accounts for Starknet
    alt Starknet wallet exists
        Backend-->>App: Return existing wallet
    else Create new wallet
        Backend->>Privy: Create new Starknet wallet via walletApi
        Backend->>Privy: Optionally fetch public key
        Backend->>App: Return new wallet details (address, publicKey)
    end
    App->>App: Store wallet in StarknetContext & localStorage
    App-->>User: Wallet ready
Loading
sequenceDiagram
    participant User
    participant App
    participant Backend
    participant Privy
    participant Paymaster
    participant StarknetRPC as Starknet RPC

    User->>App: Initiate Starknet transaction (order/transfer)
    App->>App: Retrieve Starknet wallet (address, publicKey, walletId)
    App->>App: Get access token & prepare tx details
    App->>Backend: POST /api/starknet/create-order|transfer (JWT, wallet, tx params)
    Backend->>Backend: Validate JWT & request body
    Backend->>StarknetRPC: Check wallet deployment status (class hash)
    Backend->>Backend: Resolve classHash & determine origin
    Backend->>Privy: Fetch wallet via getStarknetWallet
    Backend->>Paymaster: setupPaymaster (validate config & fees)
    Backend->>Backend: buildReadyAccount (construct signer with Privy signing)
    Backend->>Backend: Encode Cairo calldata & construct calls
    alt Account not deployed
        Backend->>Paymaster: deployReadyAccount (estimate fee with 1.5x margin)
    else Account deployed
        Backend->>Paymaster: Estimate transaction fee
    end
    Backend->>Paymaster: Execute paymaster transaction
    Backend->>StarknetRPC: Wait for confirmation & extract orderId from events
    Backend-->>App: Return txHash, orderId, success status
    App->>App: Update transaction history & balance
    App-->>User: Transaction confirmed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #273: Modifies fallback token merge logic in app/utils.ts; the main PR changes that flow to explicitly skip Starknet during token population.
  • PR #256: Updates app/hooks/useSmartWalletTransfer.ts for native token handling; main PR adds Starknet transfer branch to the same hook.
  • PR #315: Modifies network data structures (app/mocks.ts networks and app/utils.ts RPC/explorer utilities); main PR introduces Starknet to these same surfaces.

Suggested labels

enhancement

Suggested reviewers

  • chibie
  • jeremy0x
  • sundayonah

Poem

🐰 A rabbit hops through Starknet's chain,
With Privy keys and paymaster's reign,
Context flows, addresses align,
No-blocks now sparkles on Cairo's line!

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.86% 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 starknet on noblocks' is vague and lacks specificity about the main changes; while it mentions Starknet, it doesn't convey the comprehensive nature of the integration (API routes, wallet management, balance handling, etc.). Consider a more descriptive title like 'Add Starknet Layer 2 support with wallet, transfer, and order APIs' to better reflect the scope and main objectives of the changeset.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description covers major changes, references the linked issue, and includes implementation details; however, the Testing and Checklist sections show incomplete items (unchecked boxes and 'see loom here' without verification details).
Linked Issues check ✅ Passed The PR implements the key technical requirements from issue #321: Starknet is exposed as a selectable network, API endpoints enable wallet creation and transactions, and raw signing via Privy is utilized for non-EVM operations.
Out of Scope Changes check ✅ Passed All major changes align with #321 objectives; minor additions (StarknetContext, hooks, utility functions) are supporting implementations for Starknet integration and do not introduce unrelated features.

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

✨ Finishing touches
  • 📝 Generate docstrings

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
Contributor

@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: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (2)
app/components/MobileDropdown.tsx (1)

102-104: Missing Starknet balance handling - inconsistent with WalletDetails.tsx.

The activeBalance derivation doesn't account for Starknet, unlike WalletDetails.tsx (lines 79-83) which includes allBalances.starknetWallet for Starknet networks. This could show incorrect balances when on Starknet.

Suggested fix for consistency
   const activeBalance = isInjectedWallet
     ? allBalances.injectedWallet
-    : allBalances.smartWallet;
+    : selectedNetwork.chain.name === "Starknet"
+      ? allBalances.starknetWallet
+      : allBalances.smartWallet;
app/components/TransferForm.tsx (1)

190-192: Missing starknetWalletBalance in useEffect dependency array.

The effect uses starknetWalletBalance (line 170) but it's not in the dependency array. This can cause stale data when Starknet balance updates don't trigger a re-fetch of transferNetworkBalance.

🐛 Proposed fix
   }, [transferNetwork, user?.linkedAccounts]);
+  }, [transferNetwork, user?.linkedAccounts, starknetWalletBalance]);
🤖 Fix all issues with AI agents
In @app/api/starknet/create-wallet/route.ts:
- Around line 47-71: The existence check for a Starknet wallet (privy.getUser
and existingStarknetWallet logic) swallows errors and allows creation to
proceed; change the catch in the try/catch that wraps privy.getUser so that on
error you stop the flow and return an error response (e.g., NextResponse.json
with success: false and a 500 status) instead of continuing to create a new
wallet, ensuring the route does not create duplicates when the check fails; keep
the error log (console.error) but immediately return after logging to abort
further processing.

In @app/api/starknet/get-public-key/route.ts:
- Around line 57-61: The response currently returns the full wallet object
alongside publicKey in the NextResponse.json call; remove the wallet field to
avoid leaking sensitive internal data and return only the minimal fields (e.g.,
success and publicKey). Locate the return statement that constructs the JSON
(the block returning { success: true, publicKey: publicKey, wallet: wallet })
and modify it to omit wallet (or replace it with a safe, explicit field such as
walletAddress if you must return a derived public identifier), ensuring only
publicKey (and success) are sent back.
- Around line 49-54: The error response in the get-public-key route currently
returns the full wallet object; remove the wallet property from the
NextResponse.json error response so sensitive wallet details are not exposed.
Locate the return that calls NextResponse.json({ error: "...", wallet: wallet },
{ status: 404 }) in the route handler and change it to only return the error
message (and status 404); if you need the wallet for diagnostics keep it for
internal logging but do not include it in the JSON response.
- Around line 32-55: The route currently fetches a wallet by walletId but never
verifies ownership against the authenticated user (authUserId), so add an
authorization check after obtaining the wallet: compare wallet.userId (or
wallet.ownerId/public_owner field if present) to authUserId retrieved earlier
and if they do not match return a 403 JSON response; ensure you also handle the
case where getWallet returns null/undefined by returning 404 before the
ownership check. Use the existing symbols wallet, authUserId, and
privy.walletApi.getWallet to locate where to add the check and the early error
responses.

In @app/context/StarknetContext.tsx:
- Around line 208-234: refreshWalletState calls getAccessToken() but doesn't
handle a null token; update refreshWalletState to check the returned token
before using it (e.g., if token is null/undefined, return early or skip the
fetch and optionally clear auth-dependent state), and ensure the Authorization
header is only set when token is present (or omit header entirely when token is
null) — adjust the logic inside refreshWalletState (referencing getAccessToken,
refreshWalletState, and the fetch to /api/starknet/wallet-state) accordingly.
- Around line 135-206: The createWallet function must guard against a missing
access token and ensure its declared return type matches actual returns: after
calling getAccessToken() (in createWallet), check if token is null/undefined and
setError/throw a descriptive Error before using it in Authorization headers; and
for the wallet id return value either change the function signature to
Promise<string | null> or enforce a non-null newWalletId by throwing an Error if
newWalletId is null (so callers always receive a string). Update setError/throw
messages accordingly and adjust any call sites or types that rely on
createWallet returning a non-null string.
- Around line 45-74: The effect incorrectly assumes a linked Privy wallet is
deployed and uses loose typing; update the useEffect handling of
user.linkedAccounts to (1) stop setting deployed=true and remove deployed from
saveToLocalStorage here—deployment must be determined via an on‑chain check or
by calling deployReadyAccount from app/lib/starknet.ts instead, and (2) replace
the (account: any) callback with a proper type or a type guard matching the
linked account shape (e.g., an interface with type, chainType/chain_type, id,
address, publicKey/public_key) so the find and property accesses are type-safe
(apply the same typing pattern used in app/api/starknet/create-wallet/route.ts).

In @app/hooks/useSmartWalletTransfer.ts:
- Around line 137-192: In useSmartWalletTransfer, add validations for
Starknet-specific fields before making the API call: ensure
starknetWallet.address is present and non-null (validate
starknetWallet.address), ensure process.env.NEXT_PUBLIC_STARKNET_READY_CLASSHASH
(classHash) is defined and non-empty, and verify starknetWallet.deployed ===
true (reject/throw if not deployed); if any check fails, set an appropriate
error via setError, call trackEvent("Transfer failed", ...) with the failure
reason, and throw an Error to stop execution before calling getAccessToken or
fetch.

In @app/lib/authorization.ts:
- Around line 5-34: In getUserAuthorizationKey, remove the incorrect string cast
for res.expiresAt and parse it as a number (e.g., Number(res.expiresAt)) when
computing expiresAt; avoid using the shared "unknown" fallback for cacheKey —
require userId or derive a per-request key (e.g., hash userJwt) so entries for
anonymous requests don’t collide; and replace the unbounded userSignerCache Map
with a bounded cache (implement a simple LRU or size-limited eviction around
userSignerCache) to prevent unbounded memory growth when storing entries from
generateUserSigner.

In @app/mocks.ts:
- Around line 4-26: The StarknetMainnet mock incorrectly constructs the chain ID
and leaves the RPC URL without a fallback: replace the id generation in the
starknetMainnet object (id property) with the correct Starknet Mainnet chain ID
hex string "0x534e5f4d41494e" (do not use toHex or BigInt on the literal), and
update rpcUrls.default.http and rpcUrls.public.http to use
process.env.NEXT_PUBLIC_STARKNET_RPC_URL with a sensible fallback (e.g., a known
public Starknet mainnet RPC URL) so the value is never undefined at runtime.

In @app/pages/TransactionForm.tsx:
- Around line 122-126: The balance selection in TransactionStatus.tsx is
inverted and missing the Starknet network check; update the logic that computes
activeBalance (or similarly named variable) to match TransactionForm.tsx: if
isInjectedWallet use injectedWalletBalance, else if selectedNetwork.chain.name
=== "Starknet" use starknetWalletBalance, else use smartWalletBalance; locate
the balance selection in TransactionStatus.tsx (around where activeBalance or
balance is derived, lines ~348-350) and replace the inverted condition with this
three-way ternary/if sequence so injected wallets are prioritized and Starknet
uses starknetWalletBalance.

In @app/pages/TransactionPreview.tsx:
- Around line 198-277: The Starknet branch inside the TransactionPreview flow
(the if checking selectedNetwork.chain.name === "Starknet" && !isInjectedWallet)
completes the order creation but does not return, so execution falls through
into the injected/smart wallet paths and duplicates work; fix it by adding an
early return immediately after the Starknet success sequence (for example right
after setIsOrderCreated(true) and trackEvent(...)) so the function exits once
the Starknet order is created and prevents calling the injected wallet/smart
wallet logic and getOrderId() again.
🟡 Minor comments (9)
app/hooks/useCNGNRate.ts-44-51 (1)

44-51: Update comment to reflect actual behavior.

The comment states "(testnet, not supported by aggregator)" but the code skips rate fetching for all Starknet networks (both testnet and mainnet). According to the PR objectives, Starknet Mainnet was also added. The comment should clarify that this applies to all Starknet networks, not just testnet.

📝 Suggested comment update
-    // Skip rate fetching for Starknet networks (testnet, not supported by aggregator)
+    // Skip rate fetching for Starknet networks (not yet supported by aggregator)
     if (network.toLowerCase().includes("starknet")) {
app/hooks/useCNGNRate.ts-116-120 (1)

116-120: Update comment and consider more robust network detection.

Same as the hook above—the comment mentions "testnet" but applies to all Starknet networks. Additionally, using .includes("starknet") for network detection could match unintended network names (e.g., "not-starknet-compatible" would also match).

📝 Suggested improvements

Fix the comment:

-    // Skip rate fetching for Starknet networks (testnet, not supported by aggregator)
+    // Skip rate fetching for Starknet networks (not yet supported by aggregator)
     if (network.toLowerCase().includes("starknet")) {

Optional: More robust network detection (if network naming conventions are well-defined):

-    if (network.toLowerCase().includes("starknet")) {
+    const isStarknetNetwork = network.toLowerCase().startsWith("starknet") || 
+                              network.toLowerCase() === "starknet sepolia" ||
+                              network.toLowerCase() === "starknet mainnet";
+    if (isStarknetNetwork) {

Or consider using a helper function for network type detection if this pattern is repeated across the codebase.

app/context/TokensContext.tsx-75-76 (1)

75-76: Misleading comment about Starknet handling.

The comment states "Skip Starknet since we already handled it above", but there is no Starknet-specific handling visible in the preceding code (lines 34-74).

Update the comment to accurately reflect the reason for skipping Starknet, such as "Skip Starknet as its tokens are managed separately" or provide the actual location where Starknet tokens are handled.

📝 Proposed fix for the comment
       // Merge fallback tokens for any networks missing from API response
       Object.keys(FALLBACK_TOKENS).forEach((networkName) => {
-        // Skip Starknet since we already handled it above
+        // Skip Starknet as it uses a separate token management flow
         if (networkName === "Starknet") return;
app/api/starknet/create-wallet/route.ts-32-42 (1)

32-42: Remove unused request body parsing and redundant validation.

Line 33 parses the request body but never uses it. Lines 37-42 perform a redundant check since userId is assigned from authUserId, which was already validated at line 25.

🧹 Proposed cleanup
-// Get request body
-const body = await request.json();
-
 const userId = authUserId;

-if (!userId) {
-  return NextResponse.json(
-    { error: "No user ID available" },
-    { status: 400 },
-  );
-}
-
 // First, check if user already has a Starknet wallet
app/hooks/useWalletAddress.ts-22-29 (1)

22-29: Unhandled exception risk from normalizeStarknetAddress.

According to app/utils.ts (lines 746-774), normalizeStarknetAddress throws errors for invalid addresses (e.g., missing 0x prefix, invalid hex characters, address too long). If starknetAddress from context is malformed, this hook will throw an unhandled exception.

Consider wrapping in a try-catch for graceful degradation:

Suggested defensive handling
   // If on Starknet, return Starknet wallet address
   if (selectedNetwork?.chain?.name === "Starknet") {
     if (!starknetAddress) {
       return undefined;
     }
-    const address = normalizeStarknetAddress(starknetAddress);
-    return address ?? starknetAddress;
+    try {
+      return normalizeStarknetAddress(starknetAddress);
+    } catch {
+      // Fallback to raw address if normalization fails
+      return starknetAddress;
+    }
   }
app/api/starknet/transfer/route.ts-128-130 (1)

128-130: BigInt conversion can throw for invalid input.

BigInt(amount) will throw a SyntaxError if amount is not a valid integer string (e.g., contains decimals, non-numeric characters, or is empty). Consider validating the amount format before conversion.

Suggested validation
+    // Validate amount is a valid integer string
+    if (!/^\d+$/.test(String(amount))) {
+      return NextResponse.json(
+        { error: "Invalid amount format: must be a non-negative integer string" },
+        { status: 400 },
+      );
+    }
+
     // Convert amount to u256 format
     const amountU256 = cairo.uint256(BigInt(amount));
app/utils.ts-564-565 (1)

564-565: Potential precision loss for large token balances.

Converting BigInt to Number via Number(balanceInWei) can lose precision for values exceeding Number.MAX_SAFE_INTEGER (2^53 - 1). While unlikely for typical balances, very large holdings could show incorrect values.

Consider using string-based division or a BigInt-safe decimal library for precision-critical calculations:

// Alternative approach using string manipulation for precision
const balanceStr = balanceInWei.toString();
const decimals = token.decimals;
const intPart = balanceStr.slice(0, -decimals) || '0';
const decPart = balanceStr.slice(-decimals).padStart(decimals, '0');
const balance = parseFloat(`${intPart}.${decPart}`);
app/pages/TransactionPreview.tsx-272-276 (1)

272-276: Inconsistent property name in trackEvent.

Line 275 uses result.transaction_hash but the response from the API (line 276 in route.ts) returns transactionHash. This will result in undefined being tracked.

🐛 Proposed fix
         trackEvent("Swap started", {
           "Entry point": "Transaction preview",
           "Wallet type": "Starknet embedded wallet",
-          "Transaction hash": result.transaction_hash,
+          "Transaction hash": result.transactionHash,
         });
app/lib/starknet.ts-285-289 (1)

285-289: Empty string fallback for RPC URL can cause cryptic errors.

If NEXT_PUBLIC_STARKNET_RPC_URL is not set, the empty string will be used, leading to confusing network errors rather than a clear configuration error.

🐛 Proposed fix
 export function getRpcProvider() {
   const rpcUrl = process.env.NEXT_PUBLIC_STARKNET_RPC_URL || "";
+  if (!rpcUrl) {
+    throw new Error("NEXT_PUBLIC_STARKNET_RPC_URL environment variable is required");
+  }

   return new RpcProvider({ nodeUrl: rpcUrl });
 }
🧹 Nitpick comments (13)
app/api/starknet/create-wallet/route.ts (1)

73-89: Consider adding wallet creation validation.

The route returns the wallet immediately after creation without verifying the operation succeeded or the wallet is in a usable state. Additionally, the multiple property fallbacks (public_key || publicKey, chainType || chain_type) suggest API response inconsistency.

Consider:

  1. Adding explicit validation that required wallet properties are present before returning
  2. Investigating the Privy API response schema to use proper TypeScript types instead of as any
  3. Documenting why property fallbacks are necessary (API versioning, inconsistent responses, etc.)
app/api/starknet/transfer/route.ts (2)

198-214: Transaction confirmation could block indefinitely.

account.waitForTransaction() without a timeout could hang indefinitely if the transaction is stuck or the network is slow. The current catch block only logs and continues, but this doesn't prevent the request from timing out at the infrastructure level.

Consider adding a timeout or documenting the expected behavior.

Suggested timeout handling
     // Wait for transaction confirmation
     try {
-      const txReceipt = await account.waitForTransaction(
-        result.transaction_hash,
-      );
+      const txReceipt = await Promise.race([
+        account.waitForTransaction(result.transaction_hash),
+        new Promise((_, reject) =>
+          setTimeout(() => reject(new Error("Transaction confirmation timeout")), 60000)
+        ),
+      ]);

       if (!txReceipt.isSuccess()) {

210-214: Use console.warn for warning messages.

Line 211 uses console.log for a warning message. Consider using console.warn for consistency with the message severity.

Minor fix
     } catch (error) {
-      console.log(
+      console.warn(
         "[API] Warning: Could not confirm transaction, but it may still succeed",
       );
     }
app/lib/authorization.ts (2)

36-49: Consider explicit error handling over silent empty string.

Returning an empty string (Line 48) when signature generation fails may lead to silent failures downstream. Consider returning null or throwing an error to make failures explicit and easier to debug.

♻️ Proposed alternative
 export function buildAuthorizationSignature({
   input,
   authorizationKey,
 }: {
   input: WalletApiRequestSignatureInput;
   authorizationKey: string;
 }): string {
+  if (!input || !authorizationKey) {
+    throw new Error("Missing required parameters for authorization signature");
+  }
+
   const signature = generateAuthorizationSignature({
     input,
     authorizationPrivateKey: authorizationKey,
   });

-  return signature ?? "";
+  if (!signature) {
+    throw new Error("Failed to generate authorization signature");
+  }
+  return signature;
 }

1-49: Consider adding JSDoc documentation.

Since this file exports a public API (getUserAuthorizationKey and buildAuthorizationSignature), adding JSDoc comments would improve developer experience and document expected behavior, parameters, and return values.

app/context/StarknetContext.tsx (1)

76-103: Consider more robust localStorage handling.

  1. Boolean parsing (Line 95): The check storedDeployed === "true" is correct but could be more explicit. Consider using JSON.parse or explicitly checking for "false" as well.

  2. Missing error handling: localStorage access can fail in private browsing or when storage is full. Consider wrapping in try/catch to prevent crashes.

♻️ Proposed improvements
   useEffect(() => {
+    try {
       if (authenticated && user?.id) {
         const storedWalletId = localStorage.getItem(
           `${STORAGE_PREFIX}walletId_${user.id}`,
         );
         // ... other getItem calls ...
         const storedDeployed = localStorage.getItem(
           `${STORAGE_PREFIX}deployed_${user.id}`,
         );

         if (storedWalletId) setWalletId(storedWalletId);
         if (storedAddress) setAddress(storedAddress);
         if (storedPublicKey) setPublicKey(storedPublicKey);
-        if (storedDeployed === "true") setDeployed(true);
+        setDeployed(storedDeployed === "true");
       } else if (!user) {
         setWalletId(null);
         setAddress(null);
         setPublicKey(null);
         setDeployed(false);
         setError(null);
       }
+    } catch (error) {
+      console.error("Failed to load Starknet wallet state from localStorage:", error);
+    }
   }, [authenticated, user]);
app/hooks/useSmartWalletTransfer.ts (1)

238-262: Consider more robust error type detection.

The error type detection (Lines 256-260) relies on string matching (includes("429"), includes("HTTP")), which could be fragile if error message formats change. Consider using error codes or more structured error properties if available from the underlying libraries.

app/api/starknet/create-order/route.ts (4)

51-52: Inconsistent variable naming convention.

WalletAddress uses PascalCase while other destructured variables use camelCase. This should be walletAddress for consistency.

♻️ Suggested fix
-    address: WalletAddress,
+    address: walletAddress,
   } = body;

   const provider = getRpcProvider();
   try {
-    await provider.getClassHashAt(WalletAddress);
+    await provider.getClassHashAt(walletAddress);
     isDeployed = true;

Also update line 242:

-          address: starknetAddress,
+          address: walletAddress,

114-114: Potential confusion between publicKey from body and walletPublicKey from wallet lookup.

The code extracts publicKey from the request body (line 40) but then fetches walletPublicKey from getStarknetWallet (line 114) and uses only the latter. Consider clarifying intent or removing the unused publicKey from destructuring if it's not needed.


256-262: Extract magic hex string to a named constant.

The event key 0x3427759bfd3b607... should be a named constant for maintainability and clarity about what event it represents.

♻️ Suggested fix
+const ORDER_CREATED_EVENT_KEY = "0x3427759bfd3b941f14e687e129519da3c9b0046c5b9aaa290bb1dede63753b3";
+
 // In the event processing:
-            if (
-              Object.values(event.keys).includes(
-                "0x3427759bfd3b941f14e687e129519da3c9b0046c5b9aaa290bb1dede63753b3",
-              )
-            ) {
+            if (Object.values(event.keys).includes(ORDER_CREATED_EVENT_KEY)) {

247-248: Hardcoded wait variable is unused as a configurable option.

The wait variable is hardcoded to true and never configurable. Either remove it and inline the condition, or make it a request parameter if waiting should be optional.

♻️ Suggested fix
-    const wait = true;
-    if (wait) {
+    // Wait for transaction confirmation and extract orderId
+    {
       try {
         const txReceipt = await account.waitForTransaction(
app/utils.ts (1)

482-514: Consider caching CoinGecko price responses.

fetchTokenPrice makes an API call for each token on every balance fetch. CoinGecko has rate limits (10-50 calls/minute for free tier). Consider:

  1. Caching prices with a short TTL (e.g., 30-60 seconds)
  2. Batching multiple token prices into a single API call
♻️ Example caching approach
// Price cache with 60-second TTL
const priceCache: Map<string, { price: number; timestamp: number }> = new Map();
const PRICE_CACHE_TTL = 60 * 1000;

async function fetchTokenPrice(tokenSymbol: string): Promise<number | null> {
  const cached = priceCache.get(tokenSymbol);
  if (cached && Date.now() - cached.timestamp < PRICE_CACHE_TTL) {
    return cached.price;
  }
  
  // ... existing fetch logic ...
  
  if (price !== null) {
    priceCache.set(tokenSymbol, { price, timestamp: Date.now() });
  }
  return price;
}
app/lib/starknet.ts (1)

181-189: Fragile signature extraction with multiple fallback paths.

The code checks multiple nested paths for the signature (data?.signature, data?.result?.signature, etc.). This suggests uncertainty about the API response format. Consider documenting the expected response structure or adding explicit validation.

// Document expected response format from Privy
// Expected: { signature: "0x..." } or { result: { signature: "0x..." } }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ad2eb1 and 4e89666.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/logos/eth-logo.svg is excluded by !**/*.svg
  • public/logos/strk-logo.svg is excluded by !**/*.svg
📒 Files selected for processing (30)
  • .env.example
  • app/api/starknet/create-order/route.ts
  • app/api/starknet/create-wallet/route.ts
  • app/api/starknet/get-public-key/route.ts
  • app/api/starknet/transfer/route.ts
  • app/api/track-logout/route.ts
  • app/components/MainPageContent.tsx
  • app/components/MobileDropdown.tsx
  • app/components/Navbar.tsx
  • app/components/NetworkSelectionModal.tsx
  • app/components/NetworksDropdown.tsx
  • app/components/SettingsDropdown.tsx
  • app/components/TransferForm.tsx
  • app/components/WalletDetails.tsx
  • app/context/BalanceContext.tsx
  • app/context/StarknetContext.tsx
  • app/context/TokensContext.tsx
  • app/context/index.ts
  • app/hooks/useCNGNRate.ts
  • app/hooks/useSmartWalletTransfer.ts
  • app/hooks/useWalletAddress.ts
  • app/lib/authorization.ts
  • app/lib/privy.ts
  • app/lib/starknet.ts
  • app/mocks.ts
  • app/pages/TransactionForm.tsx
  • app/pages/TransactionPreview.tsx
  • app/providers.tsx
  • app/utils.ts
  • package.json
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/hooks/useWalletAddress.ts
  • app/components/NetworksDropdown.tsx
  • app/hooks/useSmartWalletTransfer.ts
  • app/components/Navbar.tsx
  • app/components/MobileDropdown.tsx
  • app/components/NetworkSelectionModal.tsx
  • app/components/WalletDetails.tsx
  • app/components/TransferForm.tsx
  • app/context/BalanceContext.tsx
  • app/components/MainPageContent.tsx
  • app/utils.ts
  • app/pages/TransactionPreview.tsx
  • app/components/SettingsDropdown.tsx
📚 Learning: 2025-07-23T07:30:23.720Z
Learnt from: jeremy0x
Repo: paycrest/noblocks PR: 177
File: app/utils.ts:179-196
Timestamp: 2025-07-23T07:30:23.720Z
Learning: The `normalizeNetworkName` function in app/utils.ts has been updated to be dynamic and scalable, converting any network identifier to sentence case with hyphens replaced by spaces and proper handling of acronyms like "BNB".

Applied to files:

  • app/context/TokensContext.tsx
  • app/components/NetworkSelectionModal.tsx
  • app/utils.ts
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/TransferForm.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/TransferForm.tsx
📚 Learning: 2025-12-05T02:22:28.063Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 312
File: app/page.tsx:7-11
Timestamp: 2025-12-05T02:22:28.063Z
Learning: In Next.js 15 with React 19, components using useSearchParams() cannot be statically prerendered. When app/layout.tsx has `export const dynamic = "force-static"`, MainPageContent must be dynamically imported with ssr: false to avoid "Element type is invalid" errors during build. The dynamic import pattern `dynamic(() => import("./components/MainPageContent").then(mod => ({ default: mod.MainPageContent })), { ssr: false })` is the correct solution for this scenario in the noblocks repository.

Applied to files:

  • app/components/MainPageContent.tsx
📚 Learning: 2025-07-23T07:28:30.491Z
Learnt from: jeremy0x
Repo: paycrest/noblocks PR: 177
File: app/utils.ts:179-196
Timestamp: 2025-07-23T07:28:30.491Z
Learning: The `normalizeNetworkName` function should be dynamic and scalable, converting any network identifier to sentence case with hyphens replaced by spaces, rather than using a hardcoded mapping.

Applied to files:

  • app/utils.ts
🧬 Code graph analysis (20)
app/api/starknet/get-public-key/route.ts (4)
app/api/starknet/create-wallet/route.ts (1)
  • POST (10-99)
app/lib/jwt.ts (1)
  • verifyJWT (11-46)
app/lib/config.ts (1)
  • DEFAULT_PRIVY_CONFIG (29-36)
app/lib/privy.ts (1)
  • getPrivyClient (9-24)
app/hooks/useWalletAddress.ts (3)
app/context/index.ts (3)
  • useInjectedWallet (6-6)
  • useStarknet (15-15)
  • useNetwork (2-2)
app/context/StarknetContext.tsx (1)
  • useStarknet (301-307)
app/utils.ts (1)
  • normalizeStarknetAddress (747-775)
app/api/starknet/create-order/route.ts (1)
app/lib/starknet.ts (5)
  • getRpcProvider (285-289)
  • getStarknetWallet (89-112)
  • setupPaymaster (294-342)
  • buildReadyAccount (195-280)
  • deployReadyAccount (348-440)
app/api/starknet/create-wallet/route.ts (3)
app/lib/jwt.ts (1)
  • verifyJWT (11-46)
app/lib/config.ts (1)
  • DEFAULT_PRIVY_CONFIG (29-36)
app/lib/privy.ts (1)
  • getPrivyClient (9-24)
app/components/NetworksDropdown.tsx (1)
app/context/StarknetContext.tsx (1)
  • useStarknet (301-307)
app/pages/TransactionForm.tsx (2)
app/context/BalanceContext.tsx (1)
  • useBalance (262-268)
app/context/index.ts (1)
  • useBalance (3-3)
app/context/StarknetContext.tsx (1)
app/context/index.ts (2)
  • StarknetProvider (15-15)
  • useStarknet (15-15)
app/hooks/useSmartWalletTransfer.ts (1)
app/hooks/analytics/analytics-utils.ts (1)
  • trackEvent (231-231)
app/components/Navbar.tsx (1)
app/hooks/useWalletAddress.ts (1)
  • useWalletAddress (11-34)
app/components/MobileDropdown.tsx (5)
app/context/StarknetContext.tsx (1)
  • useStarknet (301-307)
app/context/index.ts (2)
  • useStarknet (15-15)
  • useInjectedWallet (6-6)
app/context/InjectedWalletContext.tsx (1)
  • useInjectedWallet (114-114)
app/hooks/useWalletAddress.ts (1)
  • useWalletAddress (11-34)
app/components/index.ts (1)
  • CopyAddressWarningModal (11-11)
app/lib/authorization.ts (1)
app/lib/privy.ts (1)
  • getPrivyClient (9-24)
app/components/NetworkSelectionModal.tsx (2)
app/context/StarknetContext.tsx (1)
  • useStarknet (301-307)
app/context/index.ts (1)
  • useStarknet (15-15)
app/components/WalletDetails.tsx (2)
app/hooks/useWalletAddress.ts (1)
  • useWalletAddress (11-34)
app/utils.ts (1)
  • formatCurrency (70-83)
app/components/TransferForm.tsx (4)
app/context/BalanceContext.tsx (1)
  • useBalance (262-268)
app/context/TokensContext.tsx (1)
  • useTokens (115-121)
app/context/StarknetContext.tsx (1)
  • useStarknet (301-307)
app/utils.ts (1)
  • fetchBalanceForNetwork (697-714)
app/components/MainPageContent.tsx (1)
app/hooks/useWalletAddress.ts (1)
  • useWalletAddress (11-34)
app/utils.ts (1)
app/types.ts (2)
  • Token (209-215)
  • Network (262-270)
app/pages/TransactionPreview.tsx (4)
app/context/StarknetContext.tsx (1)
  • useStarknet (301-307)
app/context/BalanceContext.tsx (1)
  • useBalance (262-268)
app/utils.ts (1)
  • getGatewayContractAddress (791-805)
app/hooks/analytics/analytics-utils.ts (1)
  • trackEvent (231-231)
app/providers.tsx (2)
app/context/StarknetContext.tsx (1)
  • StarknetProvider (34-299)
app/context/index.ts (1)
  • StarknetProvider (15-15)
app/components/SettingsDropdown.tsx (2)
app/context/InjectedWalletContext.tsx (1)
  • useInjectedWallet (114-114)
app/hooks/useWalletAddress.ts (1)
  • useWalletAddress (11-34)
app/lib/starknet.ts (2)
app/lib/privy.ts (1)
  • getPrivyClient (9-24)
app/lib/authorization.ts (2)
  • getUserAuthorizationKey (12-34)
  • buildAuthorizationSignature (36-49)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 130-130: [UnorderedKey] The STARKNET_PAYMASTER_MODE key should go before the STARKNET_PAYMASTER_URL key

(UnorderedKey)


[warning] 131-131: [UnorderedKey] The STARKNET_PAYMASTER_API_KEY key should go before the STARKNET_PAYMASTER_MODE key

(UnorderedKey)

🔇 Additional comments (58)
app/api/track-logout/route.ts (1)

34-34: The original regex validation is correct. Starknet addresses follow a canonical format of 0x followed by exactly 64 hexadecimal characters (32 bytes, zero-padded). Official Starknet tooling enforces and returns this standardized form across the ecosystem, so the regex requiring precisely 64 characters is appropriate.

Likely an incorrect or invalid review comment.

app/context/index.ts (1)

15-15: LGTM!

The export pattern is consistent with other context providers in this file.

app/mocks.ts (1)

100-103: LGTM!

The addition of Starknet to the networks array follows the established pattern, assuming the chain definition is corrected.

.env.example (1)

117-135: LGTM! Starknet configuration is well-structured.

The new Starknet environment variables are logically grouped and well-documented. The static analysis warnings about key ordering (lines 130-131) can be safely ignored—logical grouping by functionality (paymaster URL, mode, API key) is more valuable than strict alphabetical ordering in this context.

package.json (1)

38-38: Both package versions have been verified as existing and stable with no known security advisories:

  • @privy-io/[email protected]: Exists in npm registry, published 4 months ago, well-maintained with 280 versions and multiple active maintainers. Latest stable is 1.32.5.
  • [email protected]: Exists in npm registry, published 4 weeks ago, well-maintained with 289 versions. This is the current latest stable version.

No security vulnerabilities detected in the npm registry for either package.

app/components/NetworkSelectionModal.tsx (3)

11-11: LGTM!

The import of useStarknet enables access to Starknet wallet creation functionality needed for network switching.


27-27: LGTM!

Correctly retrieves the ensureWalletExists function from the Starknet context for wallet creation during network switches.


64-64: LGTM!

Correctly passes the wallet creation function to enable Starknet wallet provisioning during network switches. The inline comment clearly documents the intent.

app/components/Navbar.tsx (3)

30-30: LGTM!

The import of useWalletAddress centralizes wallet address logic, supporting multiple wallet types (injected, Starknet, EVM smart wallet) in a single hook.


41-41: LGTM!

The hook call correctly retrieves the appropriate wallet address based on the current context (injected wallet, Starknet, or EVM smart wallet).


45-48: Excellent refactor!

The simplified activeWallet construction is much cleaner and more maintainable. By delegating address resolution to useWalletAddress, this code now handles Starknet, injected wallets, and EVM smart wallets consistently across the application.

app/components/NetworksDropdown.tsx (3)

16-16: LGTM!

Correctly imports useStarknet alongside other context hooks, maintaining consistent import organization.


30-30: LGTM!

Correctly retrieves the wallet creation function from Starknet context, consistent with the pattern used in other components.


60-60: LGTM!

Correctly passes the wallet creation function to the network switch handler, enabling automatic Starknet wallet provisioning when needed. The implementation is consistent across all network selection components.

app/api/starknet/create-wallet/route.ts (2)

12-30: LGTM!

Authentication logic correctly validates the JWT token and extracts the user ID following the established pattern.


90-98: LGTM!

The error handling appropriately catches exceptions, logs them for debugging, and returns a proper 500 response to the client.

app/components/MainPageContent.tsx (3)

22-26: Clean import reorganization for Starknet integration.

The import consolidation and addition of useWalletAddress aligns with the PR's goal to centralize wallet address derivation across injected, Starknet, and EVM wallets.

Also applies to: 44-44


64-65: Good use of the centralized wallet address hook.

Using useWalletAddress() here simplifies the PageLayout component and ensures consistent wallet address resolution across all network types (injected, Starknet, EVM).


146-168: Formatting-only changes to stateProps.

These are whitespace/formatting adjustments with no functional impact.

app/hooks/useWalletAddress.ts (1)

1-34: Well-structured centralized wallet address hook.

The hook provides a clean single source of truth for wallet addresses across all network types (injected, Starknet, EVM). The priority order (injected → Starknet → EVM smart wallet) is logical and aligns with the PR objectives.

app/lib/privy.ts (2)

7-24: Good singleton pattern implementation.

The lazy initialization with environment variable validation and error handling for the authorization key setup is well-structured. The singleton avoids repeated PrivyClient instantiation.


16-22: No action needed. The walletApi.updateAuthorizationKey() method is synchronous according to Privy's documentation—it does not return a Promise and should not be awaited. The current implementation is correct.

app/providers.tsx (1)

92-106: Correct provider hierarchy for Starknet integration.

Placing StarknetProvider after InjectedWalletProvider and before downstream providers (TokensProvider, BalanceProvider, etc.) is the right ordering. This ensures:

  • StarknetProvider has access to NetworkProvider and InjectedWalletProvider context
  • Inner providers can consume Starknet wallet state for balance fetching and transactions
app/api/starknet/transfer/route.ts (2)

13-34: Well-structured JWT authentication flow.

The authorization header validation, JWT verification, and user ID extraction follow good security practices with clear error responses for each failure case.


36-47: Wallet ownership validation is delegated to Privy, not explicitly checked locally.

The route calls getStarknetWallet(walletId) without verifying the wallet belongs to authUserId. However, wallet ownership IS implicitly validated downstream: rawSign() passes the userJwt to Privy's generateUserSigner(), which only generates a valid authorization key if the JWT matches the wallet owner. A guessed walletId would succeed at getStarknetWallet() but fail at the signing step when Privy rejects the unauthorized user.

While this is secure in practice, adding explicit ownership verification in the route handler (e.g., checking if the wallet appears in the user's wallet list before processing) would be more defensive and clearer to reviewers.

app/components/WalletDetails.tsx (4)

60-60: Good centralization of wallet address.

Using useWalletAddress() and deriving activeWallet from it simplifies the component and ensures consistent address resolution across network types.

Also applies to: 76-76


81-83: Correct balance source selection for Starknet.

The ternary properly routes to starknetWallet balances when on Starknet, falling back to smartWallet for EVM networks.


233-241: Clarify the Starknet balance display intent.

Both branches display a $ prefix, but the comment suggests Starknet shows "token amount directly" while mainnet shows "USD value." If the Starknet branch should show raw token amounts (not USD), the $ prefix may confuse users.

If the intent is to show USD value for both (just using different formatting), the comment should be updated for clarity.


376-385: Good USD conversion display for STRK and ETH.

Using balancesUsd for Starknet-native tokens ensures accurate USD-equivalent display based on current prices.

app/components/MobileDropdown.tsx (2)

45-45: Good wallet address centralization and Starknet integration.

Using useWalletAddress() for walletAddress and smartWallet, plus extracting ensureWalletExists from useStarknet, aligns with the PR's goal of unified wallet handling.

Also applies to: 52-52, 54-61


129-129: Enables Starknet wallet auto-creation on network switch.

Passing ensureWalletExists to handleNetworkSwitch allows automatic Starknet wallet creation when users switch to the Starknet network, providing a seamless onboarding experience.

app/components/SettingsDropdown.tsx (4)

26-26: LGTM! Centralized wallet address sourcing.

The new useWalletAddress hook cleanly centralizes wallet address resolution across injected wallets, Starknet, and EVM smart wallets, improving maintainability.

Also applies to: 33-34


127-131: LGTM! Proper Starknet state cleanup on logout.

The localStorage cleanup correctly removes all Starknet wallet state keys on logout, maintaining consistency with the StarknetContext's storage pattern.


46-51: LGTM! Safe walletAddress usage.

All usages of walletAddress properly handle the potentially undefined value with null coalescing operators.


136-140: The logout idempotency concern is not an issue. Privy's logout() function is idempotent and safe to call multiple times—calling it when no session is active simply becomes a no-op. The catch block pattern here is actually sound defensive programming that ensures logout completes regardless of whether the error originated from logout() itself or from disconnectWallet(). No changes needed.

Likely an incorrect or invalid review comment.

app/pages/TransactionForm.tsx (1)

67-72: LGTM! Properly destructures Starknet balance.

The expanded destructuring correctly retrieves starknetWalletBalance from the useBalance hook, aligning with the BalanceContext updates to support Starknet.

app/context/StarknetContext.tsx (2)

12-43: LGTM! Well-structured state management.

The interfaces and state setup are clean and properly typed. The STORAGE_PREFIX constant ensures consistent localStorage key naming.


274-307: LGTM! Standard React context pattern.

The provider and hook follow React best practices with proper error handling for usage outside the provider.

app/hooks/useSmartWalletTransfer.ts (4)

19-32: LGTM! Backward-compatible interface extension.

The optional starknetWallet parameter maintains backward compatibility while enabling Starknet transfer support.


107-114: LGTM! Proper chain switching for EVM-only.

Correctly skips chain switching for Starknet, which operates differently from EVM networks.

Based on learnings, selectedNetwork is always defined and does not require null safety checks.


194-209: LGTM! EVM transfer logic preserved.

The existing EVM transfer logic is correctly preserved within an else block, maintaining backward compatibility.


272-272: LGTM! Dependency array updated correctly.

The addition of starknetWallet to the dependency array ensures the transfer callback updates when Starknet wallet state changes.

app/context/BalanceContext.tsx (3)

80-104: Starknet balance fetching logic looks correct.

The early return pattern cleanly separates Starknet from EVM balance fetching. The error handling appropriately resets balance state on failure.


169-192: Missing starknetWalletBalance in useEffect dependency array.

The starknetWalletBalance is used inside fetchBalance when setting the transfer network balance (Line 170), but it's not listed in the dependency array. This could cause stale closure issues where the effect captures an outdated starknetWalletBalance value.

However, reviewing the context, this useEffect is in TransferForm.tsx, not BalanceContext.tsx. Let me re-examine.


228-236: Logout/cleanup effect correctly resets all balance states.

This effect ensures all balance states (including Starknet) are cleared when the user logs out or disconnects, preventing stale data from persisting.

app/api/starknet/create-order/route.ts (1)

202-206: Fee margin calculation is correct.

The withMargin15 function correctly implements ceiling division for 1.5x multiplier: (bi * 3 + 1) / 2 ensures proper rounding up.

app/components/TransferForm.tsx (3)

169-178: Starknet balance path correctly uses context value.

When the transfer network is Starknet, the code correctly uses the pre-fetched starknetWalletBalance from context rather than making a new fetch call. This is efficient since the balance is already maintained by BalanceContext.


366-374: Address validation correctly supports both EVM and Starknet formats.

The regex /^0x[a-fA-F0-9]{40}$|^0x[a-fA-F0-9]{64}$/ properly validates:

  • EVM addresses: 0x + 40 hex chars (42 total)
  • Starknet addresses: 0x + 64 hex chars (66 total)

The length validation and error message are also clear and helpful.


120-126: Starknet wallet data correctly passed to transfer hook.

The starknetWallet object includes all necessary fields (walletId, publicKey, address, deployed) for the transfer hook to handle Starknet transactions.

app/pages/TransactionPreview.tsx (2)

137-139: Balance selection correctly prioritizes Starknet when on Starknet network.

The ternary chain properly checks injected wallet first, then Starknet network, then falls back to smart wallet balance.


477-478: Guard condition for save transaction data is appropriate.

The check !embeddedWallet?.address && !starknetAddress correctly prevents saving when neither wallet type has an address, while still allowing the save to proceed if either exists.

app/utils.ts (3)

747-775: Well-implemented Starknet address normalization.

The function correctly:

  • Validates the 0x prefix
  • Checks for valid hex characters
  • Enforces max length of 64 hex chars
  • Left-pads shorter addresses with zeros

The documentation with examples is helpful.


973-981: Starknet wallet creation failure doesn't block network switch.

The try-catch around ensureWalletExists() allows the network switch to proceed even if wallet creation fails. This is appropriate UX - users should be able to view the Starknet network even if their wallet isn't created yet.


153-154: Starknet explorer link uses Voyager.

Voyager is the standard Starknet block explorer, this is correct.

app/lib/starknet.ts (4)

242-243: Signature splitting assumes exact 128-character hex body.

The code assumes the signature (after removing 0x) is exactly 128 hex characters (64 bytes = r + s components). If Privy returns a different format, this will silently produce incorrect signature components.

Consider adding validation:

const body = sig.slice(2);
if (body.length !== 128) {
  throw new Error(`Unexpected signature length: ${body.length}, expected 128`);
}
return [`0x${body.slice(0, 64)}`, `0x${body.slice(64)}`];

222-277: Custom signer implementation correctly overrides required methods.

The anonymous class extending RawSigner properly implements signMessage, signTransaction, and signRaw using Privy's raw signing API. The typed data hashing for paymaster signatures is correctly implemented.


294-342: Paymaster setup has appropriate validation and error handling.

The function:

  • Validates required configuration
  • Enforces API key requirement for sponsored mode
  • Checks paymaster availability before returning
  • Resolves gas token for non-sponsored mode

This is a robust implementation.


417-421: Fee estimation only runs when there are calls to execute.

The condition !isSponsored && calls.length > 0 correctly skips fee estimation for sponsored transactions and deploy-only operations.

Comment on lines +47 to +71
try {
const user = await privy.getUser(userId);
const linkedAccounts = user.linkedAccounts || [];
const existingStarknetWallet = linkedAccounts.find(
(account: any) =>
account.type === "wallet" &&
(account.chainType === "starknet" ||
account.chain_type === "starknet"),
);

if (existingStarknetWallet) {
const wallet = existingStarknetWallet as any;
return NextResponse.json({
success: true,
wallet: {
id: wallet.id,
address: wallet.address,
publicKey: wallet.public_key || wallet.publicKey,
chainType: wallet.chainType || wallet.chain_type,
},
});
}
} catch (error) {
console.error("Error checking for existing Starknet wallet:", error);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent duplicate wallet creation when existence check fails.

If the existing wallet check fails due to an error (lines 69-71), the route silently continues and creates a new wallet. This can result in multiple Starknet wallets for the same user if a wallet exists but the API call failed.

🔧 Proposed fix to handle check errors properly
 try {
   const user = await privy.getUser(userId);
   const linkedAccounts = user.linkedAccounts || [];
   const existingStarknetWallet = linkedAccounts.find(
     (account: any) =>
       account.type === "wallet" &&
       (account.chainType === "starknet" ||
         account.chain_type === "starknet"),
   );

   if (existingStarknetWallet) {
     const wallet = existingStarknetWallet as any;
     return NextResponse.json({
       success: true,
       wallet: {
         id: wallet.id,
         address: wallet.address,
         publicKey: wallet.public_key || wallet.publicKey,
         chainType: wallet.chainType || wallet.chain_type,
       },
     });
   }
 } catch (error) {
   console.error("Error checking for existing Starknet wallet:", error);
+  // Don't create a new wallet if we can't verify existing wallet
+  return NextResponse.json(
+    { error: "Failed to check for existing wallet" },
+    { status: 500 }
+  );
 }
📝 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
try {
const user = await privy.getUser(userId);
const linkedAccounts = user.linkedAccounts || [];
const existingStarknetWallet = linkedAccounts.find(
(account: any) =>
account.type === "wallet" &&
(account.chainType === "starknet" ||
account.chain_type === "starknet"),
);
if (existingStarknetWallet) {
const wallet = existingStarknetWallet as any;
return NextResponse.json({
success: true,
wallet: {
id: wallet.id,
address: wallet.address,
publicKey: wallet.public_key || wallet.publicKey,
chainType: wallet.chainType || wallet.chain_type,
},
});
}
} catch (error) {
console.error("Error checking for existing Starknet wallet:", error);
}
try {
const user = await privy.getUser(userId);
const linkedAccounts = user.linkedAccounts || [];
const existingStarknetWallet = linkedAccounts.find(
(account: any) =>
account.type === "wallet" &&
(account.chainType === "starknet" ||
account.chain_type === "starknet"),
);
if (existingStarknetWallet) {
const wallet = existingStarknetWallet as any;
return NextResponse.json({
success: true,
wallet: {
id: wallet.id,
address: wallet.address,
publicKey: wallet.public_key || wallet.publicKey,
chainType: wallet.chainType || wallet.chain_type,
},
});
}
} catch (error) {
console.error("Error checking for existing Starknet wallet:", error);
// Don't create a new wallet if we can't verify existing wallet
return NextResponse.json(
{ error: "Failed to check for existing wallet" },
{ status: 500 }
);
}
🤖 Prompt for AI Agents
In @app/api/starknet/create-wallet/route.ts around lines 47 - 71, The existence
check for a Starknet wallet (privy.getUser and existingStarknetWallet logic)
swallows errors and allows creation to proceed; change the catch in the
try/catch that wraps privy.getUser so that on error you stop the flow and return
an error response (e.g., NextResponse.json with success: false and a 500 status)
instead of continuing to create a new wallet, ensuring the route does not create
duplicates when the check fails; keep the error log (console.error) but
immediately return after logging to abort further processing.

Comment on lines +32 to +55
// Get request body
const body = await request.json();
const { walletId } = body;

if (!walletId) {
return NextResponse.json(
{ error: "walletId is required" },
{ status: 400 }
);
}

const privy = getPrivyClient();
const wallet: any = await privy.walletApi.getWallet({ id: walletId });

const publicKey = wallet.public_key || wallet.publicKey;

if (!publicKey) {
return NextResponse.json({
error: "Public key not available from Privy. " +
"This may require enabling Starknet support in your Privy dashboard or " +
"upgrading to a Privy tier that supports Starknet public key access.",
wallet: wallet,
}, { status: 404 });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Missing authorization check for wallet access.

The route authenticates the user and extracts authUserId (line 23) but never verifies that the requested walletId belongs to that user. This allows any authenticated user to query public keys and wallet details for wallets they don't own.

🔒 Proposed fix to add wallet ownership verification
 const privy = getPrivyClient();
+
+// Verify the wallet belongs to the authenticated user
+const user = await privy.getUser(authUserId);
+const userWallet = user.linkedAccounts?.find(
+  (account: any) => account.id === walletId
+);
+
+if (!userWallet) {
+  return NextResponse.json(
+    { error: "Wallet not found or does not belong to user" },
+    { status: 403 }
+  );
+}
+
 const wallet: any = await privy.walletApi.getWallet({ id: walletId });
📝 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
// Get request body
const body = await request.json();
const { walletId } = body;
if (!walletId) {
return NextResponse.json(
{ error: "walletId is required" },
{ status: 400 }
);
}
const privy = getPrivyClient();
const wallet: any = await privy.walletApi.getWallet({ id: walletId });
const publicKey = wallet.public_key || wallet.publicKey;
if (!publicKey) {
return NextResponse.json({
error: "Public key not available from Privy. " +
"This may require enabling Starknet support in your Privy dashboard or " +
"upgrading to a Privy tier that supports Starknet public key access.",
wallet: wallet,
}, { status: 404 });
}
// Get request body
const body = await request.json();
const { walletId } = body;
if (!walletId) {
return NextResponse.json(
{ error: "walletId is required" },
{ status: 400 }
);
}
const privy = getPrivyClient();
// Verify the wallet belongs to the authenticated user
const user = await privy.getUser(authUserId);
const userWallet = user.linkedAccounts?.find(
(account: any) => account.id === walletId
);
if (!userWallet) {
return NextResponse.json(
{ error: "Wallet not found or does not belong to user" },
{ status: 403 }
);
}
const wallet: any = await privy.walletApi.getWallet({ id: walletId });
const publicKey = wallet.public_key || wallet.publicKey;
if (!publicKey) {
return NextResponse.json({
error: "Public key not available from Privy. " +
"This may require enabling Starknet support in your Privy dashboard or " +
"upgrading to a Privy tier that supports Starknet public key access.",
wallet: wallet,
}, { status: 404 });
}
🤖 Prompt for AI Agents
In @app/api/starknet/get-public-key/route.ts around lines 32 - 55, The route
currently fetches a wallet by walletId but never verifies ownership against the
authenticated user (authUserId), so add an authorization check after obtaining
the wallet: compare wallet.userId (or wallet.ownerId/public_owner field if
present) to authUserId retrieved earlier and if they do not match return a 403
JSON response; ensure you also handle the case where getWallet returns
null/undefined by returning 404 before the ownership check. Use the existing
symbols wallet, authUserId, and privy.walletApi.getWallet to locate where to add
the check and the early error responses.

Comment on lines +49 to +54
return NextResponse.json({
error: "Public key not available from Privy. " +
"This may require enabling Starknet support in your Privy dashboard or " +
"upgrading to a Privy tier that supports Starknet public key access.",
wallet: wallet,
}, { status: 404 });
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove wallet object from error response.

Similar to the success case, the error response should not include the entire wallet object to avoid exposing sensitive details.

🔒 Proposed fix
 return NextResponse.json({
   error: "Public key not available from Privy. " +
           "This may require enabling Starknet support in your Privy dashboard or " +
           "upgrading to a Privy tier that supports Starknet public key access.",
-  wallet: wallet,
 }, { status: 404 });
📝 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
return NextResponse.json({
error: "Public key not available from Privy. " +
"This may require enabling Starknet support in your Privy dashboard or " +
"upgrading to a Privy tier that supports Starknet public key access.",
wallet: wallet,
}, { status: 404 });
return NextResponse.json({
error: "Public key not available from Privy. " +
"This may require enabling Starknet support in your Privy dashboard or " +
"upgrading to a Privy tier that supports Starknet public key access.",
}, { status: 404 });
🤖 Prompt for AI Agents
In @app/api/starknet/get-public-key/route.ts around lines 49 - 54, The error
response in the get-public-key route currently returns the full wallet object;
remove the wallet property from the NextResponse.json error response so
sensitive wallet details are not exposed. Locate the return that calls
NextResponse.json({ error: "...", wallet: wallet }, { status: 404 }) in the
route handler and change it to only return the error message (and status 404);
if you need the wallet for diagnostics keep it for internal logging but do not
include it in the JSON response.

Comment on lines +57 to +61
return NextResponse.json({
success: true,
publicKey: publicKey,
wallet: wallet,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid exposing the entire wallet object in the response.

Returning the complete wallet object may expose sensitive internal details beyond the public key. Consider returning only the necessary public key field.

🔒 Proposed fix to return only necessary fields
 return NextResponse.json({
   success: true,
   publicKey: publicKey,
-  wallet: wallet,
 });
📝 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
return NextResponse.json({
success: true,
publicKey: publicKey,
wallet: wallet,
});
return NextResponse.json({
success: true,
publicKey: publicKey,
});
🤖 Prompt for AI Agents
In @app/api/starknet/get-public-key/route.ts around lines 57 - 61, The response
currently returns the full wallet object alongside publicKey in the
NextResponse.json call; remove the wallet field to avoid leaking sensitive
internal data and return only the minimal fields (e.g., success and publicKey).
Locate the return statement that constructs the JSON (the block returning {
success: true, publicKey: publicKey, wallet: wallet }) and modify it to omit
wallet (or replace it with a safe, explicit field such as walletAddress if you
must return a derived public identifier), ensuring only publicKey (and success)
are sent back.

Comment on lines +45 to +74
useEffect(() => {
if (authenticated && user) {
const linkedAccounts = user.linkedAccounts || [];
const starknetWallet = linkedAccounts.find(
(account: any) =>
account.type === "wallet" &&
(account.chainType === "starknet" ||
account.chain_type === "starknet"),
);

if (starknetWallet) {
const wallet = starknetWallet as any;
const walletId = wallet.id || null;
const address = wallet.address || null;
const pk = wallet.publicKey || wallet.public_key;

setWalletId(walletId);
setAddress(address);
if (pk) setPublicKey(pk);
setDeployed(true);

saveToLocalStorage({
walletId,
address,
publicKey: pk,
deployed: true,
});
}
}
}, [authenticated, user]);
Copy link
Contributor

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

🌐 Web query:

Privy linkedAccounts API wallet deployment status

💡 Result:

I couldn't find a specific "wallet deployment status" field in Privy's linkedAccounts docs. Do you mean:

  • whether a linked wallet's smart-account/contract has been deployed on-chain, or
  • whether a wallet is linked in the user.linkedAccounts array?

If you mean the latter: linked wallets appear on the user object under user.linkedAccounts (see Privy docs). (docs.privy.io)

If you mean on-chain deployment status (smart account deployed vs. not): Privy's docs and integrations describe EIP-7702 (delegation) vs. owner-style smart accounts and wallet creation issues are surfaced on the status page, but I didn't find a documented linkedAccounts field that reports deployment state — you typically check on-chain (or via Privy wallet APIs/status endpoints). (alchemy.com)

Which of those do you need? I can search the Privy API docs for any exact "deployed"/"deploymentStatus" field and return endpoints and example responses.

Citations:


🏁 Script executed:

# Check for type definitions and usage of linkedAccounts in the codebase
fd -t ts -t tsx | xargs grep -l "linkedAccounts" | head -10

Repository: paycrest/noblocks

Length of output: 230


🏁 Script executed:

# Look for user type definition and linkedAccounts type
ast-grep --pattern 'interface $_ {
  $$$
  linkedAccounts
  $$$
}'

Repository: paycrest/noblocks

Length of output: 43


🏁 Script executed:

# Search for how deployed status is used elsewhere in the codebase
rg "deployed" -t ts -t tsx -B 2 -A 2 | head -50

Repository: paycrest/noblocks

Length of output: 88


🏁 Script executed:

# Search for linkedAccounts and user type definitions
rg "linkedAccounts" --max-count 20

Repository: paycrest/noblocks

Length of output: 2046


🏁 Script executed:

# Search for interface/type definitions of User or account-related types
rg "interface.*[Uu]ser|type.*[Uu]ser" | head -30

Repository: paycrest/noblocks

Length of output: 2221


🏁 Script executed:

# Check how the deployed flag is used in other parts of the codebase
rg "deployed" -B 3 -A 3 | head -80

Repository: paycrest/noblocks

Length of output: 4536


🏁 Script executed:

# Look at the full StarknetContext file to understand the deployed flag's purpose
fd "StarknetContext"

Repository: paycrest/noblocks

Length of output: 92


🏁 Script executed:

cat -n app/api/starknet/create-wallet/route.ts

Repository: paycrest/noblocks

Length of output: 3617


🏁 Script executed:

# Search for any on-chain deployment checking or verification logic
rg "deploy" -i -B 2 -A 2 | head -100

Repository: paycrest/noblocks

Length of output: 4515


Remove incorrect deployed status assumption and improve type safety.

  1. Deployed assumption is incorrect (Line 64): Finding a wallet in Privy's linkedAccounts does not mean it's deployed on-chain. The linkedAccounts API only indicates a wallet is linked to the user account; deployment status must be checked separately on-chain. The deployReadyAccount function in app/lib/starknet.ts shows that deployment is an explicit, separate step. Always setting deployed: true here will cause incorrect state in business logic (e.g., TransferForm).

  2. Type safety (Line 49): Replace (account: any) with a proper type definition or type guard to maintain type safety. The same pattern appears in app/api/starknet/create-wallet/route.ts (line 51).

🤖 Prompt for AI Agents
In @app/context/StarknetContext.tsx around lines 45 - 74, The effect incorrectly
assumes a linked Privy wallet is deployed and uses loose typing; update the
useEffect handling of user.linkedAccounts to (1) stop setting deployed=true and
remove deployed from saveToLocalStorage here—deployment must be determined via
an on‑chain check or by calling deployReadyAccount from app/lib/starknet.ts
instead, and (2) replace the (account: any) callback with a proper type or a
type guard matching the linked account shape (e.g., an interface with type,
chainType/chain_type, id, address, publicKey/public_key) so the find and
property accesses are type-safe (apply the same typing pattern used in
app/api/starknet/create-wallet/route.ts).

Comment on lines +137 to +192
if (selectedNetwork.chain.name === "Starknet") {
// Handle Starknet transfer via API route
if (!starknetWallet?.walletId || !starknetWallet?.publicKey) {
const error = "Starknet wallet not configured";
setError(error);
trackEvent("Transfer failed", {
Amount: amount,
"Send token": token,
"Recipient address": recipientAddress,
Network: selectedNetwork.chain.name,
"Reason for failure": error,
"Transfer date": new Date().toISOString(),
});
throw new Error(error);
}

// Get access token for API authentication
const accessToken = await getAccessToken();
if (!accessToken) {
throw new Error("Failed to get access token");
}

// Get class hash from environment
const classHash = process.env.NEXT_PUBLIC_STARKNET_READY_CLASSHASH;

// Convert amount to wei (smallest unit)
const amountInWei = parseUnits(
amount.toString(),
tokenDecimals,
).toString();

// Call Starknet transfer API
const response = await fetch("/api/starknet/transfer", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
walletId: starknetWallet.walletId,
publicKey: starknetWallet.publicKey,
classHash,
tokenAddress,
amount: amountInWei,
recipientAddress,
address: starknetWallet.address,
}),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || "Transfer failed");
}

hash = data.transactionHash;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation for required Starknet parameters.

The Starknet transfer path has several validation gaps:

  1. Missing address validation (Line 182): starknetWallet.address is used without checking if it's null. The validation at Line 139 only checks walletId and publicKey.

  2. Missing classHash validation (Line 160): process.env.NEXT_PUBLIC_STARKNET_READY_CLASSHASH is used without verifying it's defined.

  3. No deployment check: Should verify starknetWallet.deployed is true before attempting a transfer, as undeployed accounts may not be able to transact.

🔧 Proposed fixes
         if (selectedNetwork.chain.name === "Starknet") {
           // Handle Starknet transfer via API route
-          if (!starknetWallet?.walletId || !starknetWallet?.publicKey) {
+          if (
+            !starknetWallet?.walletId ||
+            !starknetWallet?.publicKey ||
+            !starknetWallet?.address
+          ) {
             const error = "Starknet wallet not configured";
             setError(error);
             trackEvent("Transfer failed", {
               Amount: amount,
               "Send token": token,
               "Recipient address": recipientAddress,
               Network: selectedNetwork.chain.name,
               "Reason for failure": error,
               "Transfer date": new Date().toISOString(),
             });
             throw new Error(error);
           }

           // Get access token for API authentication
           const accessToken = await getAccessToken();
           if (!accessToken) {
             throw new Error("Failed to get access token");
           }

           // Get class hash from environment
           const classHash = process.env.NEXT_PUBLIC_STARKNET_READY_CLASSHASH;
+          if (!classHash) {
+            throw new Error("Starknet class hash not configured");
+          }

           // Convert amount to wei (smallest unit)
           const amountInWei = parseUnits(
             amount.toString(),
             tokenDecimals,
           ).toString();

           // Call Starknet transfer API
           const response = await fetch("/api/starknet/transfer", {
             method: "POST",
             headers: {
               "Content-Type": "application/json",
               Authorization: `Bearer ${accessToken}`,
             },
             body: JSON.stringify({
               walletId: starknetWallet.walletId,
               publicKey: starknetWallet.publicKey,
               classHash,
               tokenAddress,
               amount: amountInWei,
               recipientAddress,
               address: starknetWallet.address,
             }),
           });
🤖 Prompt for AI Agents
In @app/hooks/useSmartWalletTransfer.ts around lines 137 - 192, In
useSmartWalletTransfer, add validations for Starknet-specific fields before
making the API call: ensure starknetWallet.address is present and non-null
(validate starknetWallet.address), ensure
process.env.NEXT_PUBLIC_STARKNET_READY_CLASSHASH (classHash) is defined and
non-empty, and verify starknetWallet.deployed === true (reject/throw if not
deployed); if any check fails, set an appropriate error via setError, call
trackEvent("Transfer failed", ...) with the failure reason, and throw an Error
to stop execution before calling getAccessToken or fetch.

Comment on lines +5 to +34
// In-memory cache of user authorization keys to avoid regenerating per request.
// Keyed by userId; values include key and expiry.
const userSignerCache = new Map<
string,
{ authorizationKey: string; expiresAt: number }
>();

export async function getUserAuthorizationKey({
userJwt,
userId,
}: {
userJwt: string;
userId?: string;
}): Promise<string> {
const cacheKey = userId || "unknown";
const cached = userSignerCache.get(cacheKey);
const now = Date.now();
if (cached && cached.expiresAt > now + 5_000) {
// 5s safety buffer
return cached.authorizationKey;
}
const privy = getPrivyClient();
const res = await privy.walletApi.generateUserSigner({
userJwt: userJwt,
});
const authKey = res.authorizationKey;
const expiresAt = new Date(res.expiresAt as unknown as string).getTime();
userSignerCache.set(cacheKey, { authorizationKey: authKey, expiresAt });
return authKey;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Privy walletApi generateUserSigner method response type expiresAt field documentation

💡 Result:

The Wallet API's user-signer/authenticate response includes an expires_at numeric timestamp. Privy's docs (Authenticate endpoint) show the field as a number and include an example value (1697059200000) that is a Unix timestamp in milliseconds; the endpoint docs' field name is expires_at. [1]

Note: the textual field description in parts of the docs can be ambiguous about seconds vs. milliseconds — rely on the example (milliseconds) and treat expires_at as a Unix epoch timestamp in milliseconds when parsing the response. [1][2]

Sources:

  • Authenticate (Wallets) — response schema and example (expires_at). [1]
  • Using user signers / SDK behavior (context for ephemeral user authorization keys). [2]

References
[1] Privy API — Authenticate endpoint (wallets/authenticate) response.
[2] Privy Docs — Using user signers / Wallet SDK.


🏁 Script executed:

# Search for how getPrivyClient is defined and used
rg "getPrivyClient" -A 5 -B 2

# Search for other uses of getUserAuthorizationKey to understand userId parameter
rg "getUserAuthorizationKey" -B 2 -A 3

# Look for cache management patterns in the codebase
rg "Map<.*expiresAt|userSignerCache" -B 3 -A 3

# Check if there are other similar caches with size limits
rg "new Map|Cache|cache" app/lib/ -l

Repository: paycrest/noblocks

Length of output: 7589


🏁 Script executed:

# Check the full context of the authorization.ts file
wc -l app/lib/authorization.ts

Repository: paycrest/noblocks

Length of output: 88


🏁 Script executed:

# Check if there are Privy type definitions in the codebase
fd "privy" app/ -name "*.d.ts" -o -name "*.ts" | head -20

# Look at the actual usage of getUserAuthorizationKey to see if userId is always provided
rg "getUserAuthorizationKey" -B 5 -A 5

# Check what the actual response type might be
rg "generateUserSigner" -B 2 -A 5

# Look for any type definitions related to Privy response
rg "authorizationKey|expiresAt" -B 3 -A 1

Repository: paycrest/noblocks

Length of output: 5171


Fix type casting and address caching concerns.

  1. Incorrect type casting (Line 31): Privy's generateUserSigner returns expiresAt as a numeric Unix timestamp in milliseconds, not a string. Remove the unnecessary as unknown as string cast. Change to:

    const expiresAt = Number(res.expiresAt);
    
  2. Cache key collision risk (Line 19): Using "unknown" as a fallback means requests without userId share the same cache entry, which is incorrect. While userId is typically provided in practice, the fallback should either require userId or use a hash of userJwt as the cache key to ensure proper isolation.

  3. Unbounded cache growth: The Map has no size limit or eviction policy, which could cause memory issues in production with many users. Consider implementing an LRU cache or size limit.

🤖 Prompt for AI Agents
In @app/lib/authorization.ts around lines 5 - 34, In getUserAuthorizationKey,
remove the incorrect string cast for res.expiresAt and parse it as a number
(e.g., Number(res.expiresAt)) when computing expiresAt; avoid using the shared
"unknown" fallback for cacheKey — require userId or derive a per-request key
(e.g., hash userJwt) so entries for anonymous requests don’t collide; and
replace the unbounded userSignerCache Map with a bounded cache (implement a
simple LRU or size-limited eviction around userSignerCache) to prevent unbounded
memory growth when storing entries from generateUserSigner.

Comment on lines +4 to +26
// Define Starknet Mainnet chain (not in viem by default)
export const starknetMainnet = {
id: BigInt(toHex('SN_MAIN')).toString(), // Starknet Mainnet chain ID (SN_MAIN encoded)
name: "Starknet",
network: "starknet-mainnet",
nativeCurrency: {
decimals: 18,
name: "Ether",
symbol: "ETH",
},
rpcUrls: {
default: {
http: [process.env.NEXT_PUBLIC_STARKNET_RPC_URL],
},
public: {
http: [process.env.NEXT_PUBLIC_STARKNET_RPC_URL],
},
},
blockExplorers: {
default: { name: "Voyager", url: "https://voyager.online" },
},
testnet: false,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect Starknet chain ID generation.

The chain ID generation on Line 6 is incorrect:

id: BigInt(toHex('SN_MAIN')).toString()

Issues:

  1. toHex('SN_MAIN') incorrectly treats the string literal 'SN_MAIN' as a numeric value to convert. The toHex function from viem expects a number or bigint, not a string. This will either throw an error or produce an unexpected result.
  2. Starknet Mainnet uses a specific chain ID constant (typically '0x534e5f4d41494e' for the hex-encoded string 'SN_MAIN').
  3. The RPC URL on Line 16 references an environment variable without a fallback, which could be undefined at runtime.
🔧 Proposed fix for chain ID and RPC URL
 // Define Starknet Mainnet chain (not in viem by default)
 export const starknetMainnet = {
-  id: BigInt(toHex('SN_MAIN')).toString(), // Starknet Mainnet chain ID (SN_MAIN encoded)
+  id: '0x534e5f4d41494e', // Starknet Mainnet chain ID (SN_MAIN in hex)
   name: "Starknet",
   network: "starknet-mainnet",
   nativeCurrency: {
     decimals: 18,
     name: "Ether",
     symbol: "ETH",
   },
   rpcUrls: {
     default: {
-      http: [process.env.NEXT_PUBLIC_STARKNET_RPC_URL],
+      http: [process.env.NEXT_PUBLIC_STARKNET_RPC_URL || "https://starknet-mainnet.public.blastapi.io"],
     },
     public: {
-      http: [process.env.NEXT_PUBLIC_STARKNET_RPC_URL],
+      http: [process.env.NEXT_PUBLIC_STARKNET_RPC_URL || "https://starknet-mainnet.public.blastapi.io"],
     },
   },
   blockExplorers: {
     default: { name: "Voyager", url: "https://voyager.online" },
   },
   testnet: false,
 };

Verify the correct Starknet Mainnet chain ID format with the starknet.js library documentation:

What is the correct chain ID format for Starknet Mainnet in starknet.js version 9.x?
🤖 Prompt for AI Agents
In @app/mocks.ts around lines 4 - 26, The StarknetMainnet mock incorrectly
constructs the chain ID and leaves the RPC URL without a fallback: replace the
id generation in the starknetMainnet object (id property) with the correct
Starknet Mainnet chain ID hex string "0x534e5f4d41494e" (do not use toHex or
BigInt on the literal), and update rpcUrls.default.http and rpcUrls.public.http
to use process.env.NEXT_PUBLIC_STARKNET_RPC_URL with a sensible fallback (e.g.,
a known public Starknet mainnet RPC URL) so the value is never undefined at
runtime.

Comment on lines 122 to +126
const activeBalance = isInjectedWallet
? injectedWalletBalance
: smartWalletBalance;
: selectedNetwork.chain.name === "Starknet"
? starknetWalletBalance
: smartWalletBalance;
Copy link
Contributor

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:

cat -n app/pages/TransactionForm.tsx | head -150 | tail -50

Repository: paycrest/noblocks

Length of output: 2076


🏁 Script executed:

# Search for useNetwork hook and isInjectedWallet definitions
rg -nP 'useNetwork|isInjectedWallet' -g '*.tsx' -g '*.ts' -A 2 | head -100

Repository: paycrest/noblocks

Length of output: 5951


🏁 Script executed:

# Search for Starknet-specific balance handling
rg -nP 'starknetWalletBalance|Starknet' -g '*.tsx' -g '*.ts' -B 2 -A 2

Repository: paycrest/noblocks

Length of output: 38237


🏁 Script executed:

# Look for where activeBalance is used
rg -nP 'activeBalance' -g '*.tsx' -g '*.ts' -B 1 -A 1

Repository: paycrest/noblocks

Length of output: 3468


🏁 Script executed:

# Search for network name constants to verify "Starknet" string
rg -nP 'chain\.name|network.*name' -g '*.tsx' -g '*.ts' | head -50

Repository: paycrest/noblocks

Length of output: 4343


The balance selection logic in TransactionForm.tsx is correct and consistent with other components. When using an injected wallet on Starknet, the code correctly prioritizes injectedWalletBalance over the network-specific starknetWalletBalance—injected wallets are network-agnostic by design.

However, there is a critical bug in TransactionStatus.tsx (lines 348-350) where the balance selection logic is inverted and lacks the Starknet network check. This should match the pattern used in TransactionForm.tsx, WalletDetails.tsx, and TransactionPreview.tsx.

🤖 Prompt for AI Agents
In @app/pages/TransactionForm.tsx around lines 122 - 126, The balance selection
in TransactionStatus.tsx is inverted and missing the Starknet network check;
update the logic that computes activeBalance (or similarly named variable) to
match TransactionForm.tsx: if isInjectedWallet use injectedWalletBalance, else
if selectedNetwork.chain.name === "Starknet" use starknetWalletBalance, else use
smartWalletBalance; locate the balance selection in TransactionStatus.tsx
(around where activeBalance or balance is derived, lines ~348-350) and replace
the inverted condition with this three-way ternary/if sequence so injected
wallets are prioritized and Starknet uses starknetWalletBalance.

Comment on lines +198 to +277
// Check if on Starknet and not using injected wallet
if (selectedNetwork.chain.name === "Starknet" && !isInjectedWallet) {
if (!walletId || !publicKey || !starknetAddress) {
toast.error(
"Starknet wallet not found. Please create a wallet first.",
);
return;
}

const params = await prepareCreateOrderParams();
setCreatedAt(new Date().toISOString());

const classHash = process.env.NEXT_PUBLIC_STARKNET_READY_CLASSHASH;

const token = await getAccessToken();
if (!token) {
throw new Error("Failed to get access token");
}

toast.loading("Creating order...");

// Execute the transaction using Starknet paymaster via API
const response = await fetch("/api/starknet/create-order", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
walletId,
publicKey,
classHash,
userId: user?.id,
origin: window.location.origin,
tokenAddress: tokenAddress,
gatewayAddress: getGatewayContractAddress(
selectedNetwork.chain.name,
) as string,
amount: params.amount.toString(),
rate: params.rate.toString(),
senderFeeRecipient: params.senderFeeRecipient as string,
senderFee: params.senderFee.toString(),
refundAddress: params.refundAddress ?? "",
messageHash: params.messageHash,
address: starknetAddress,
}),
});

toast.dismiss();

if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to create order");
}

const result = await response.json();
const orderId = result.orderId;

setOrderId(orderId);

await saveTransactionData({
orderId: orderId,
txHash: result.transactionHash,
});

setCreatedAt(new Date().toISOString());
setTransactionStatus("pending");
setCurrentStep("status");

toast.success("Order created successfully");

refreshBalance();
setIsOrderCreated(true);

trackEvent("Swap started", {
"Entry point": "Transaction preview",
"Wallet type": "Starknet embedded wallet",
"Transaction hash": result.transaction_hash,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Missing return after Starknet order creation causes duplicate code execution.

After the Starknet order creation block completes successfully (line 270), the code falls through to execute the injected wallet check (line 279) and smart wallet path (lines 361-414), then calls getOrderId() again (line 416) and shows duplicate success toasts (line 418).

Add a return statement after the Starknet block to prevent this.

🐛 Proposed fix
         trackEvent("Swap started", {
           "Entry point": "Transaction preview",
           "Wallet type": "Starknet embedded wallet",
           "Transaction hash": result.transaction_hash,
         });
+
+        return; // Exit early after Starknet order creation
       }

       if (isInjectedWallet && injectedProvider) {
🤖 Prompt for AI Agents
In @app/pages/TransactionPreview.tsx around lines 198 - 277, The Starknet branch
inside the TransactionPreview flow (the if checking selectedNetwork.chain.name
=== "Starknet" && !isInjectedWallet) completes the order creation but does not
return, so execution falls through into the injected/smart wallet paths and
duplicates work; fix it by adding an early return immediately after the Starknet
success sequence (for example right after setIsOrderCreated(true) and
trackEvent(...)) so the function exits once the Starknet order is created and
prevents calling the injected wallet/smart wallet logic and getOrderId() again.

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.

Add Starknet Network Support on Noblocks via Privy Tier 2 chain

2 participants