Skip to content

stable → main - #564

Merged
5ran6 merged 1 commit into
mainfrom
stable
Jul 6, 2026
Merged

stable → main#564
5ran6 merged 1 commit into
mainfrom
stable

Conversation

@5ran6

@5ran6 5ran6 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator
  • Added a new API endpoint for exporting Starknet embedded wallets, utilizing Privy's HPKE encryption.
  • Introduced a modal for user interaction during the export process, allowing users to copy their private key securely.
  • Updated context and hooks to manage the export modal state and integrate with the existing Starknet provider.
  • Added necessary dependencies for HPKE encryption and decryption processes.

This feature enhances the wallet's functionality by enabling secure export of private keys for Starknet users.

  • refactor(wallet): update authorization signature handling in Starknet export route
  • Replaced the previous authorization signature generation method with a new approach using and .
  • Enhanced error handling for signature generation failures, returning a 500 status with an appropriate error message.
  • Cleaned up the code for better readability and maintainability by consolidating signature logic into a single section.
  • feat(wallet): enhance ExportStarknetWalletModal with key verification
  • Added expectedPublicKey prop to verify the exported key against the wallet's signer key.
  • Implemented key verification logic to warn users if the exported key does not match the expected public key or if the derived address differs from the displayed address.
  • Introduced a key verification warning message in the modal to inform users of potential mismatches.
  • Updated context to pass the publicKey to the ExportStarknetWalletModal for verification purposes.

Port the tiered KYC API, UI, and migration changes onto latest main while preserving current main exports and dependency baseline to keep the PR isolated and reviewable.

  • fix(build): lazy Supabase admin client and restore Starknet providers
  • Defer Supabase client creation until first use so next build does not require secrets at bundle time.\n- Restore HomeTransactionFormModeProvider and StarknetProvider around KYC context tree.\n- Add https-proxy-agent for Twilio server external resolution.
  • refactor(env): update Supabase keys and improve KYC handling
  • Revised .env.example to clarify the use of Supabase secret keys and added comments for better guidance.
  • Updated jest.setup.js to use the new SUPABASE_SECRET_KEY for testing.
  • Enhanced KYC API routes to improve error handling and wallet address verification.
  • Introduced new utility functions for KYC tier management and refined transaction handling logic.
  • Improved user feedback in KYC and phone verification modals for better clarity and user experience.
  • feat(swap): implement swap transaction precheck and enhance KYC handling
  • Added a new endpoint for prechecking swap transactions to verify monthly KYC limits without inserting records.
  • Introduced utility functions for swap transaction limit checks, improving error handling and user feedback.
  • Updated KYC-related API routes to enhance wallet address verification and error messaging.
  • Enhanced the .env.example file with additional configuration options for Dojah utility bill processing.
  • Improved handling of KYC document uploads and error messages in the KYC verification process.
  • Updated transaction form to support new swap functionality and ensure accurate limit checks based on user KYC tier.
  • feat(api): enhance transaction handling and KYC validation
  • Updated transaction-related API endpoints to support both onramp and offramp transaction types.
  • Improved KYC handling by integrating new validation checks for phone numbers and ID information.
  • Refactored transaction limit checks to accommodate new transaction types and ensure accurate KYC compliance.
  • Enhanced error handling and user feedback in phone verification and KYC modals.
  • Added support for keyboard search and filtering in dropdown components for better user experience.
  • Updated configuration for server external packages to resolve dependency issues.
  • feat(kyc): enhance KYC validation and transaction handling
  • Updated .env.example with clearer comments for Smile Identity configuration.
  • Modified submitSmileIDData function to include wallet address in headers for KYC submissions.
  • Improved error handling in KYC routes to provide more specific feedback for verification attempts.
  • Refactored transaction processing to support new transaction types and ensure accurate KYC compliance.
  • Enhanced transaction status checks to include new error messages and validation for onramp/offramp transactions.
  • Updated database migrations to accommodate changes in transaction types and KYC profile management.
  • refactor(transactions): normalize transaction type handling and enhance Twilio verification timeout
  • Updated transaction processing to use normalized transaction types for consistency.
  • Introduced timeout handling for Twilio verification requests to prevent hanging operations.
  • Improved error logging for Twilio verification failures, distinguishing between timeout and other errors.
  • Adjusted database migration to enforce unique constraints on transaction records more effectively.
  • refactor(profile): streamline KYC tier handling and improve display logic
  • Replaced the previous tier expansion state management with a single state for expanded tier level.
  • Updated tier display logic to use a new utility function for consistent tier labeling.
  • Enhanced the TransactionLimitModal to reflect the current KYC tier accurately and conditionally render tier information.
  • Improved user feedback in the ProfileDrawer and TransactionLimitModal for better clarity on KYC status and upgrade options.
  • refactor(profile): update icons and KYC requirements for improved clarity
  • Replaced AiPhone01Icon with TbPhoneCall in ProfileDrawer for consistency with other icons.
  • Updated KYC tier requirements from "Phone verification" to "Phone number" for clearer user understanding.
  • refactor(kyc): update KYC tier limits and improve user messaging
  • Adjusted KYC tier monthly limits in .env.example and related documentation for clarity.
  • Enhanced user messaging in TransactionLimitModal and other components to reflect updated KYC requirements.
  • Renamed tier 0 from "Free" to "Unverified" for better understanding of user status.
  • Improved handling of phone verification prompts in the transaction flow.
  • fix(kyc): update user messaging for KYC verification prompts
  • Changed the prompt from "Start" to "Get started" for improved clarity in the KYC verification process when conditions are not met.
  • refactor(kyc): enhance TransactionLimitModal messaging and layout
  • Updated the title and messaging in TransactionLimitModal to provide clearer instructions based on the user's KYC upgrade step.
  • Improved conditional rendering for user limits and verification prompts, ensuring relevant information is displayed based on the user's verification status.
  • Adjusted styling and layout for better visual clarity and user experience.
  • feat(profile): add ProfileDrawer and integrate profile access in MobileDropdown and SettingsView
  • Introduced ProfileDrawer component for user profile management.
  • Updated MobileDropdown to include state management for ProfileDrawer and added an onOpenProfile handler.
  • Enhanced SettingsView to provide a button for accessing the profile, improving user navigation and experience.
  • fix(css): prevent iOS Safari auto-zoom on form fields
  • Added CSS rules to ensure input, select, and textarea elements have a minimum font size of 16px on devices with a max-width of 640px, preventing unwanted zoom behavior in iOS Safari.

  • feat(utils): add formatUsdAmount function for formatting USD amounts in KYC UI
  • Introduced a new utility function, formatUsdAmount, to format USD amounts with up to 4 decimal places and comma separation.
  • Updated ProfileDrawer and TransactionLimitModal components to utilize formatUsdAmount for displaying monthly limits and transaction summaries, enhancing consistency in currency formatting across the application.
  • Added CSS styles to prevent 100vh clipping for mobile in the KYC modal's smart camera component.
  • Updated the KYC document upload process to only accept utility bills, removing bank statements and PDF support.
  • Refined the formatUsdAmount function to limit decimal places to 2 for better consistency in KYC UI.
  • Improved user feedback in KYC modals and transaction forms, ensuring clarity on document requirements and verification status.
  • Introduced a new KYCStatusSnapshot interface for synchronous KYC status updates, enhancing the user experience during verification processes.
  • feat: implement referral system with API endpoints and UI components (feat: implement referral system with API endpoints and UI components #433)

  • feat: implement referral system with API endpoints and UI components

  • Added new API routes for submitting and retrieving referral data.

  • Introduced referral-related types and utility functions for handling referral codes.

  • Created UI components for referral dashboard and call-to-action, enhancing user engagement.

  • Updated mobile dropdown and main page content to include referral options and modals.

  • Enhanced middleware to support new referral API routes.

  • feat: enhance referral system and error handling

  • Added optional role property to referral data structure for better clarity.

  • Improved clipboard copy functionality with error handling for referral codes and links.

  • Updated referral data retrieval to include role information for referrers and referred users.

  • Enhanced error handling in the ReferralDashboard and ReferralDashboardView components for better user feedback.

  • feat: improve referral API and dashboard functionality

  • Refactored referral API routes to utilize a new method for retrieving user IDs from requests.

  • Updated referral data retrieval to ensure accurate wallet address handling.

  • Enhanced error handling for referral code generation and transaction volume checks.

  • Improved user feedback in the ReferralDashboard and ReferralDashboardView components for better user experience.

  • Standardized handling of referral amounts to ensure consistency across the application.

  • feat: add referral volume and reward configuration to environment and API

  • Introduced new environment variables for minimum qualifying volume and reward amount in the referral system.

  • Updated types to include referral configuration parameters.

  • Refactored referral claim logic to utilize new configuration values for volume checks and reward distribution.

  • Enhanced referral data retrieval to reflect user-specific claim statuses and auto-claim functionality for eligible referrals.

  • feat: enhance referral program configuration and error handling

  • Updated .env.example to better document referral program variables.

  • Improved KYC status verification in the referral claim API with enhanced error handling for better user feedback.

  • Refactored referral configuration values to ensure proper parsing and validation in the application.

  • refactor: rename avatar image utility function and update references

  • Renamed getAvatarImage to getAvatarImageFromAddress for clarity in its purpose.
  • Updated all references to the renamed function across components, ensuring consistent usage.
  • Added comments to improve code documentation and understanding of avatar image retrieval logic.
  • fix(referral): use config reward amount and normalize self-referral check

  • fix(referral): address PR review feedback on modal UX and copy

Remove duplicate submit toast, show referral prompt on login without requiring network modal, fix unescaped apostrophe, and standardize qualifying volume copy and env default to $20.

  • fix(referral): make submit insert atomic and handle unique conflicts

Remove race-prone preflight read, return 409 on referred_wallet_address unique violations, and add a case-insensitive unique index migration.


  • Adjusted CSS properties for the smart camera component to improve height handling.
  • Enhanced KYC modal layout with updated maximum height settings for better responsiveness.
  • Modified ProfileDrawer to allow vertical scrolling, ensuring content accessibility without overflow issues.
  • Improved styling for the smart camera component within the KYC modal for a more consistent user experience.
  • fix(kyc): correct max-height CSS property in KYC modal layout
  • Updated the max-height property in the KYC modal to use 'max-h-fit' for improved layout consistency and responsiveness.
  • Ensured that the modal maintains a better visual structure when displaying content based on user needs.

  • Removed Starknet-specific checks from various components and hooks, simplifying the handling of on-ramp conditions.
  • Replaced the deprecated isValidEvmAddressCaseInsensitive function with a new validateWalletAddress function that supports both EVM and Starknet address formats.
  • Updated the RecipientDetailsForm and TransactionForm components to utilize the new validation logic, enhancing code clarity and maintainability.
  • Removed unused Starknet-related props and conditions across components to improve overall code efficiency.
  • refactor(TransactionForm): update connected wallet address handling

  • refactor(wallet): enhance useWalletAddress hook and update TransactionForm

  • refactor(TransactionForm): update wallet address validation logic


Adds an "Earn" action in the Starknet wallet drawer that lets users supply USDC and USDT to Vesu lending pools through the starkzap SDK:

  • USDC → Clearstar USDC Reactor pool
  • USDT → Prime pool

Surfaces:

  • New "Earn" action button in the wallet sidebar (Starknet only).
  • Earn modal with deposit/withdraw tabs, token selector, live APR / monthly / yearly projection, and a success view mirroring Transfer.
  • New "Earn activity" tab next to Balances and Transactions, with a per-token "Currently supplied" card and a date-grouped list of past deposits/withdrawals. Clicking a row opens a detail page with a Voyager link, mirroring the Transactions detail flow.

Integration:

  • starkzap SDK (^3.0.0) for Vesu deposit/withdraw call preparation, market metadata, and position reads.
  • next.config aliases shim out starkzap's optional peer deps so Webpack resolves cleanly without bloating node_modules.
  • Reuses existing noblocks Privy primitives (getStarknetWallet, rawSign, buildReadyAccount, setupPaymaster, deployReadyAccount). No changes to wallet creation, auth, or signing pipelines.

Includes scripts/test-starkzap.ts: a read-only integration smoke test that hits Vesu mainnet (~2s) to verify SDK call shapes and pool wiring.

  • fix(starknet/earn): drop client-supplied address, derive server-side

Address reviewer feedback from @0xLucqs on starkience#1: the deposit/withdraw routes accepted a wallet address from the request body without verifying it matched the one derived by buildReadyAccount. No attack vector (auth gates the route), but a buggy client could send a mismatched address and credit a Vesu position to the wrong owner.

Cleanest fix per his suggestion: remove the field. Compute the canonical address server-side via computeReadyAddress(walletPublicKey, classHash) using the publicKey fetched from Privy in getStarknetWallet. That value equals what buildReadyAccount derives, so mismatch is now impossible by construction. Drops a redundant validateAndParseAddress block and the unused starknet/validateAndParseAddress import; also drops the unused deployReadyAccount import in withdraw.

Client (useEarnHandler) no longer sends address in deposit/withdraw request bodies. UI readiness gate keeps the address check (it gates the button, not the payload).

  • deposit/withdraw routes: log a warning when waitForTransaction soft-fails, matching the existing pattern in app/api/starknet/transfer/route.ts instead of silently swallowing the error
  • EarnActivityDetails: add aria-label to icon-only copy button
  • EarnActivityPanel: clickable row uses motion.button (with type="button" and w-full/text-left/bg-transparent so the layout matches the prior motion.div) so the row is keyboard-operable
  • earn.ts: log getPositions failures via console.error before falling back to the empty positions array, so production debugging can distinguish "user has no position" from "fetch failed"
  • fix(starknet/earn): honest confirmation flag + lossless Starknet balance

CodeRabbit follow-up on PR #499:

  1. Deposit / withdraw responses now include a confirmed: boolean field. When waitForTransaction throws (network error, RPC hiccup), the handler still returns success: true with the tx hash so the client gets the optimistic UX, but confirmed: false so the response is honest about the on-chain state. The client can ignore the field today (preserves current UX) or surface it in a future pass.

  2. Starknet wallet balances now carry balancesInWei: Record<string, bigint> end-to-end:

    • fetchStarknetBalancesUnified (app/utils.ts) was already computing the per-token bigint internally; it now stores and returns it, mirroring the EVM path.
    • fetchStarknetBalance wrapper exposes the new field.
    • WalletBalances interface (app/context/BalanceContext.tsx) declares balancesInWei? as an optional field, so existing consumers are unaffected.
    • EarnWalletForm reads allBalances.starknetWallet?.balancesInWei?.[token] directly instead of the lossy parseAmountToBaseUnits(walletBalanceUnit.toString()) round-trip. The Max button and amount validation now use exact base units, eliminating precision loss at the 6th USDC decimal.

The WalletBalances change is backward-compatible (optional field with a BigInt("0") fallback in the consumer); other balance consumers (Transfer, swap, balance display) are unaffected.

  • fix(balances): preserve balancesInWei end-to-end on EVM and Starknet

CodeRabbit follow-up on PR #499: the previous commit added balancesInWei? to the WalletBalances interface and plumbed it through the Starknet path, but EVM balances still discarded it in buildWalletBalancesFromRaw. That made the JSDoc claim ("Populated by Starknet and EVM fetchers") misleading and left a latent precision gap for any future caller doing exact-integer math on EVM amounts.

Concretely:

  • buildWalletBalancesFromRaw now accepts an optional balancesInWei parameter and returns it on the WalletBalances object. The CNGN conversion logic is unchanged; base units bypass the float corridor entirely (correct, since CNGN <-> NGN is a display-only rate).
  • All 7 call sites in BalanceContext.tsx that build WalletBalances from a fetchWalletBalance result now pass result.balancesInWei through.
  • The two patch sites in the [cngnRate] useEffect (re-applying the rate after it resolves) preserve balancesInWei from the existing WalletBalances object.
  • fetchStarknetBalancesUnified (utils.ts) populates balanceWei on each ChainBalanceEntry, matching the EVM entries shape so per-token exact-integer math is available downstream.

All changes are backward-compatible: balancesInWei is optional on WalletBalances, and existing consumers that read only balances (display number) are unaffected. The roadmap to bridge EVM USDC into Vesu via the Earn product can now read allBalances.<chain>?.balancesInWei?.[token] on any network without further infrastructure changes.

  • fix(starknet/earn): return 502 when waitForTransaction confirmation fails

Addresses CodeRabbit's follow-up on PR #499 (after @onahprosper asked for a recheck). The previous approach (return success: true, confirmed: false) relied on a client-side check that does not exist in useEarnHandler, so withdrawals where on-chain confirmation failed were still being recorded as completed activity in the UI.

Switching to CodeRabbit's original suggestion of an HTTP 502 response: the failure is now honest at the protocol level, the existing client check (!res.ok || !data?.success) treats it correctly without any client change, and the response body still carries transactionHash so callers can point the user to the explorer.

Drops the now-unused confirmed flag from both deposit and withdraw response shapes. Same simplification applied to both routes.

  • feat(starknet/earn): drive token list from Paycrest aggregator

Addresses @onahprosper's review feedback on PR #499: replaces the hard-coded EARN_TOKENS dropdown with a runtime fetch of Paycrest's supported-token list (via the existing getNetworkTokens("Starknet") helper, which already wraps api.paycrest.io/v2/tokens with cache + fallback).

New useEarnAvailableTokens() hook in useEarnHandler.ts:

  • Fetches Paycrest's Starknet token list on mount.
  • Intersects with EARN_TOKENS (the registry of tokens we have Vesu pool mappings for) so we never expose a token without a pool.
  • Falls back to the full EARN_TOKENS list if Paycrest is unreachable or returns no Starknet tokens.

EarnWalletForm.tsx now consumes the hook for both the dropdown items and the onSelect handler (the previous name === "USDT" ? ... : ... hardcode is replaced with a lookup against the dynamic list).

Effect: when Paycrest adds a new Starknet token AND we add a matching Vesu pool mapping to EARN_TOKEN_CONFIG, the new token appears in the UI with no further code change. The EARN_TOKENS registry remains the source of truth for which tokens our backend can build calls for; it is kept static (and exported) for type-level coverage and for the position-refresh / activity-filter loops that should iterate every supported token regardless of Paycrest's current advertisement.

  • Update app/api/starknet/earn/deposit/route.ts

  • refactor(starknet): extract applySafetyMargin to shared utility; gate fee estimation on isDeployed

Addresses two CodeRabbit comments on PR #499:

  1. Fee estimation runs on undeployed accounts without deployment context, and the result is discarded on the deploy path.

    In app/api/starknet/earn/deposit/route.ts, the fee-estimation block now only runs when isDeployed && !isSponsored. On the first-deposit path (account not yet deployed), deployReadyAccount already builds its own deploy_and_invoke paymaster transaction with proper deploymentData and performs its own deployment-aware fee estimation internally, so estimating here without deploymentData produced an inaccurate fee that was never used. Withdraw is unaffected because it already rejects undeployed accounts upfront.

  2. withMargin15 was misleadingly named and duplicated across 5 sites.

    The helper actually applies a 1.5x (50%) safety margin via (bi * 3 + 1) / 2, not 15%. Extracted into a single shared applySafetyMargin(v) export in app/lib/starknet.ts with a clarifying doc comment. All 5 call sites updated to import and use it:

    • app/lib/starknet.ts:deployReadyAccount
    • app/api/starknet/transfer/route.ts
    • app/api/starknet/create-order/route.ts
    • app/api/starknet/earn/deposit/route.ts
    • app/api/starknet/earn/withdraw/route.ts

    The local duplicate copies are removed; the formula is unchanged. This
    keeps the cross-route fee-margin behaviour consistent and gives the
    helper an honest name that matches the math.

  • feat(earn): implement Earn feature with consent modal and UI components
  • Introduced EarnConsentModal to handle user consent for using the Earn feature.
  • Updated MobileDropdown and WalletDetails components to integrate Earn UI and manage user interactions.
  • Enhanced state management for Earn activities and consent handling through useEarnAccess hook.
  • Configured environment variables for Earn feature toggling.
  • fix(earn): update EarnConsentModal risk copy and enhance MobileDropdown and WalletDetails components

  • fix(earn): update risk copy in EarnConsentModal and enhance balance checks in EarnWalletForm and EarnHubView components


  • Introduced a retry mechanism for SmileID job submissions when database-related failures occur, improving user experience during backend outages.
  • Added a classification function to categorize SmileID verification failures, providing clearer feedback on the nature of errors.
  • Updated KycModal to display specific failure reasons and hints based on the error type, enhancing user guidance during document verification.
  • Improved error handling in KycModal to set and display failure messages more effectively, ensuring users receive relevant information for troubleshooting.
  • refactor(kyc): improve error classification and user feedback in SmileID verification
  • Enhanced the classifySmileIdFailure function to better categorize database-related errors, improving clarity on failure reasons.
  • Updated KycModal to provide more specific failure hints based on error types, enhancing user guidance during verification.
  • Adjusted messaging for verification failures to ensure users receive relevant and actionable feedback.
  • fix(kyc): improve attempt counter restoration for SmileID verification failures
  • Updated logic to restore the attempt counter for users experiencing infrastructure outages, regardless of job type.
  • Enhanced error handling to log failures when restoring the attempt counter, providing better visibility into issues during SmileID verification.
  • Added support for "swap" transaction type in getTransactionHistoryTypeLabel function.
  • Replaced ReferralDashboard with ReferralHubView in MobileDropdown and WalletDetails components for improved referral management.
  • Removed unused ReferralDashboard and ReferralDashboardViewSkeleton components to streamline the codebase.
  • Enhanced ReferralCTA to directly trigger the new referral view, improving user navigation.
  • fix(wallet): restore network-aware address in settings dropdown

The desktop settings dropdown dropped the useWalletAddress() hook (KYC #496) and inlined EVM-only logic, so on Starknet it showed the EVM smart-wallet address instead of the Starknet Ready account. Restore the hook (which already handles the Starknet branch) with the migration-based EVM fallback, matching WalletDetails and MobileDropdown.

  • Replace manual EVM-only walletAddress derivation in SettingsDropdown with useWalletAddress() hook so the Starknet address is shown/copied when on Starknet network
  • Filter Starknet out of supportedNetworksDisplayed in CopyAddressWarningModal when the active network is EVM, so Starknet is never listed as a deposit option for an EVM address
  • Restore scroll position after KYC "Let's go!" closes the dialog — SmileID's camera component displaces document.scrollY; a requestAnimationFrame scrollTo(0) fires after Headless UI unlocks scroll
  • fix(settings): update wallet address logic to handle Starknet network
  • Introduced useNetwork context to determine the selected network.
  • Adjusted wallet address derivation to ensure that an EVM address is not shown when on the Starknet network, improving user experience and accuracy in address display.
  • Simplified fallback logic for wallet address to prioritize network-aware handling.

  • Introduced a new KYC policy page with comprehensive details on data handling and user privacy.
  • Updated the main content to include the new KYC policy route.
  • Enhanced the sitemap to include the KYC policy URL for better SEO.
  • Modified the KycModal to link to the new KYC policy instead of the privacy policy.
  • Added a CI workflow for continuous integration and testing on main and stable branches.
  • feat(ci): configure environment variables for CI workflow
  • Added environment variables for various services including database, API keys, and URLs to the CI workflow.
  • Included dummy values for build-time variables that are not stored in repository secrets to ensure successful builds.
  • fix(ci): update Privy SDK environment variables in CI workflow
  • Changed dummy values for NEXT_PUBLIC_PRIVY_APP_ID and PRIVY_JWKS_URL to valid placeholders required by the Privy SDK.
  • Ensured that the CI workflow is correctly configured for integration with the Privy service.
  • Replaced the KYC status check with a direct query to Supabase for user verification, enhancing reliability.
  • Updated error handling to provide clearer messages regarding KYC requirements.
  • Refactored the CopyAddressWarningModal and EarnConsentModal components to improve checkbox styling and accessibility.
  • Enhanced the KycModal to restore scroll position after closing, ensuring a smoother user experience.
  • Added a useEffect in MobileDropdown to reset the view state when the dropdown closes, improving navigation consistency.
  • refactor(ui): enhance checkbox accessibility and scroll restoration in modals
  • Updated checkbox styling in CopyAddressWarningModal for improved focus visibility and accessibility.
  • Introduced a new function in KycModal to restore scroll position after closing, enhancing user experience during modal transitions.
  • feat(MobileDropdown): reset nested views on sheet close
  • Added a useEffect hook to reset the current view to "wallet" and clear selected activities when the dropdown sheet closes. This ensures a consistent starting point for users when reopening the dropdown.
  • Introduced a new checkPartyQualification function to streamline KYC and volume checks for both claimants and referees.
  • Updated error responses to provide clearer messages based on the qualification subject (claimant or referee).
  • Refactored the tryClaimOne function to utilize the new qualification checks, improving code readability and maintainability.
  • Adjusted comments and documentation to reflect the changes in the claim process and qualification requirements.
  • refactor(referral): improve error handling and qualification checks in claim process
  • Enhanced the checkPartyQualification function to include timeout handling and improved error messages for failed rate lookups.
  • Updated the tryClaimOne function to streamline qualification checks and provide clearer error responses when verification fails.
  • Refactored the logic to ensure that invalid NGN rate payloads are properly handled, improving overall robustness of the claim process.
  • Adjusted comments for better clarity on the qualification checks and their implications in the claim workflow.

  • refactor(MobileDropdown): streamline component structure and improve view handling
  • Simplified the rendering logic within the MobileDropdown component by removing unnecessary motion animations and restructuring the JSX for better readability.
  • Enhanced the handling of different views (wallet, referrals, earn) to ensure consistent overflow behavior and improved user experience.
  • Updated the DialogPanel and view components to maintain a cleaner layout and better manage state transitions.
  • refactor(TransferForm): improve recipient address validation and error handling
  • Added dirtyFields to form state to track changes in the recipient address field.
  • Introduced a new showRecipientAddressError variable to conditionally display error messages based on the recipient address's validity and its dirty state.
  • Updated the useEffect hook to trigger validation only when the recipient address is not empty, enhancing performance and user experience.
  • Move scroll restoration out of KycModal's self-cancelling 500ms timer into AnimatedModal via AnimatePresence onExitComplete (opt-in restoreScrollOnClose). The old timer was cleared by the component's own unmount cleanup, racing the exit animation, and only covered two of the modal's close paths.
  • Restore the scroll position captured at open instead of jumping to top.
  • Remove the outer AnimatePresence/conditional around the KycModal mounts so the exit animation (and scroll restore) actually runs; drop TransactionLimitModal's early return-null for the same reason.
  • Force-refresh KYC status when ProfileDrawer and TransactionLimitModal open so a tier upgrade landing via the SmileID webhook is not hidden by the context's 30s staleness cache.
  • Sync KYCContext when KycModal's poller detects the tier upgrade (previously the modal could show success while the rest of the app showed the old tier).

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

  • fix(kyc): enforce one verified identity per phone/ID and harden SmileID callback
  • Add pending_phone_number staging: send-otp no longer overwrites the verified phone_number (or de-verifies the profile); verify-otp promotes the pending number only after the OTP is confirmed. Previously a user who started (but never finished) verifying a replacement number kept their tier and the API reported the unverified number as verified.
  • Enforce one verified profile per phone number (send-otp pre-check, verify-otp promotion guard) and per ID document (smile-id route), with partial unique indexes created by migration when no historical duplicates exist. Previously the same phone/ID could verify unlimited wallets, multiplying monthly limits.
  • Harden the SmileID callback: reject stale/unparseable timestamps (the HMAC only covers timestamp+partner_id, so captured signatures replay forever) and confirm the job outcome via SmileID's signed job_status API before any tier promotion instead of trusting unsigned body fields.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

  • fix(referral,earn): correct modal gating, pending totals, and per-user earn consent

Referral modal:

  • Suppress for ANY referral relationship, not just role === 'referred', so existing users who have referred others no longer see the new-user modal.
  • Require a known account age within 30 days; previously a missing createdAt skipped the new-user check entirely and let existing accounts through.

Network selector (root cause of 'referral popup only after refresh'): gate the open trigger on Privy 'ready' and re-check once the embedded wallet address settles, so a fresh signup reliably opens the network modal — the referral modal chains off its close callback.

Referral Pending/Earned cards: coerce reward_amount to Number in the API totals (Postgres numeric deserializes as string via PostgREST, so the reduce was string-concatenating and corrupting total_pending), and harden the card formatting against non-numeric values.

Earn disclaimer: key the 'seen' flag per Privy user id instead of a single device-global key, so a second user on the same device (and brand-new signups) see the risk disclosure instead of inheriting the first user's acceptance.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

  • fix: verification-pass corrections for KYC and network-modal gating
  • smile-id route: keep idNumberToStore undefined (not null) when SmileID returns no ID number, so the update no longer nulls out a previously stored id_number (regression introduced with the uniqueness check).
  • smile-id callback: accept SmileID's string 'true' job flags when confirming job_status, matching how the rest of the codebase treats these fields.
  • Network modal dismissal: wire the never-called markNetworkModalDismissed() into NetworkSelectionModal.handleClose, and centralize the hasSeenNetworkModal key in networkModalStore with a canonical lowercased form (legacy checksummed keys still honored on read). Previously MigrationBannerWrapper's two gates were both dead — the live store signal never fired and the storage fallback lowercased a key written checksummed — so migration UI gated on network-modal dismissal could not appear in-session, and the fresh-signup reset in Navbar removed a key form that was never written.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

  • fix: update phone number placeholder text for consistency
  • Changed the placeholder text in the PhoneVerificationModal from "enter your phone number" to "Enter your phone number" to ensure consistent capitalization and improve user experience.
  • verify-otp: gate phone promotion on the current pending_phone_number and treat zero updated rows as a superseded verification, so a stale OTP can no longer promote an old number over a newer pending request.
  • NetworkSelectionModal: replace the sticky hasCheckedStorage boolean with a wallet-scoped sentinel so eligibility re-evaluates after logout/login or a wallet switch.
  • networkModalStore: wrap localStorage access in try/catch helpers so restricted-storage environments degrade gracefully instead of throwing.
  • smileID: only accept 0/1 or an http(s) URL for SMILE_IDENTITY_SERVER(_MODE); unrecognized values now surface the configuration error instead of silently defaulting to sandbox.
  • smile-id callback: the unsigned body is now used for routing only — the job outcome AND the full_name/date_of_birth enrichment come exclusively from SmileID's signed job_status response.
  • migration: RAISE EXCEPTION (fail deploy) when historical duplicate verified identities exist, instead of warning and skipping the unique indexes.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

  • fix: update phone number placeholder text for consistency
  • Changed the placeholder text in the PhoneVerificationModal from "enter your phone number" to "Enter your phone number" to ensure consistent capitalization and improve user experience.
  • fix: update Earn disclosure URL for improved accessibility
  • Changed the Earn risk disclosure article link from a Google Document to a dedicated blog page on noblocks.xyz, enhancing user access to important information.

  • fix(ux): stop network modal re-opening on login and pin body scroll behind locked modals
  • NetworkSelectionModal: pass the raw wallet address into hasSeenNetworkModalFlag — pre-lowercasing it defeated the legacy checksummed-key fallback, so the modal re-opened on login for every pre-existing account (regression from the wallet-scoped sentinel fix). Only the sentinel comparison is case-normalized now.
  • AnimatedModal: replace restore-scroll-on-close with a body scroll lock (position: fixed at the captured offset) held from open through the end of the exit animation. The document can no longer be dragged to the bottom by modal content (e.g. the SmileID camera), and the visible jump from the old after-the-fact scrollTo restore is gone — the page never moves. Refcounted so overlapping locked modals share one pin; released on exit complete with an unmount safety net. Prop renamed restoreScrollOnClose -> lockBodyScroll.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

Fixed 2 file(s) based on 1 unresolved review comment.


  • fix(referral): carry ?ref= code from share links into the referral modal

Share links are generated as ?ref=NBXXXX (handleCopyLink) but nothing ever consumed the parameter — the only reader of ref was the BlockFest hook — so users arriving via a referral link still had to type the code manually.

  • Capture ?ref= on landing in MainPageContent (before login, since OAuth can drop the query string) and persist it as a pending code in localStorage.
  • Pre-fill the ReferralInputModal input from the pending code when it opens, and re-open the modal for a link-carried code even if this wallet dismissed it before (clicking a referral link is explicit intent).
  • Clear the pending code on successful submit and on explicit skip/backdrop close so a declined code cannot keep re-triggering the modal.
  • Shared format guard (NB + 4 alphanumerics) keeps BlockFest's ?ref=blockfest flow untouched and replaces the inline regex in the modal.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

  • AnimatedModal body lock: snapshot pre-existing inline body styles on first acquire and restore them on final release instead of blanking them; acquire the lock in an isomorphic layout effect so the pin lands before the open modal's first paint (no gap for mount-time autofocus/camera scroll).
  • networkModalStore: match the hasSeenNetworkModal key case-insensitively on read and clear, so legacy keys in any historical address casing are honored regardless of how Privy cases the address across hydration/login.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

  • fix(kyc): update transaction status filters to accurately reflect monthly spend limits
  • Adjusted the status filters for offramp and onramp transactions to include 'pending', 'fulfilling', 'fulfilled', and 'completed' statuses, ensuring that all relevant transactions contribute to the monthly spend limit.
  • Updated the SQL migration to relax the transactions.status CHECK constraint, allowing for accurate status updates without constraint violations.
  • Enhanced the logic in the transaction summary API to align with the new status definitions, improving the accuracy of spend calculations.
  • refactor(kyc, referral): normalize currency casing and improve transaction filtering
  • Introduced a normalization function for currency values to ensure consistent casing across different sources, addressing discrepancies between "cNGN" and "CNGN".
  • Updated transaction filtering logic in both the KYC transaction summary and referral claim APIs to utilize Sets for improved performance and clarity.
  • Adjusted transaction processing to apply normalization when handling currency values, enhancing the accuracy of USD calculations and overall transaction integrity.
  • feat(login): implement body scroll pinning during Privy login flow
  • Introduced a new hook, useLoginWithScrollPin, to manage body scroll locking while the Privy login dialog is active, preventing unwanted scrolling on mobile devices.
  • Updated AnimatedModal to default lockBodyScroll to true, ensuring modals maintain scroll lock by default.
  • Refactored Navbar and TransactionForm components to utilize the new loginWithScrollPin function for improved user experience during login.
  • fix(login): enhance login flow with in-flight guard and error handling
  • Added an in-flight guard to prevent multiple rapid login attempts while the pin is held.
  • Implemented error handling during the login process to ensure proper release of the scroll lock in case of failures.
  • Referrers should only require one qualifying transaction

  • Replace Noblocks logos in Navbar with SVG image and adjust dropdown arrow position. Add new SVG logo file for Noblocks World Cup logo.

  • feat(kyc): enhance KYC status and phone verification flow

  • Updated the KYC status API to include the user's full name in the response.
  • Modified the PhoneVerificationModal to conditionally display a full name input field based on the user's existing data.
  • Improved user prompts and information displayed during the phone verification process to enhance clarity and user experience.
  • Adjusted KYC context to manage full name state alongside other KYC details.
  • feat(icons): add animated Noblocks logo and World Cup assets
  • Introduced a new animated icon component that cycles through three states: the default Noblocks "n" icon, a spinning soccer ball, and the World Cup trophy.
  • Created separate components for the Noblocks World Cup logo and the animated icon, ensuring cross-browser compatibility by injecting SVGs directly into the DOM.
  • Updated the Navbar to utilize the new animated logo and World Cup logo components, enhancing the visual appeal and user experience.
  • Added CSS animations for the spinning effect and implemented reduced motion preferences for accessibility.
  • refactor(icons): enhance NoblocksAnimatedIcon for reduced motion support and update World Cup logo SVG
  • Improved the NoblocksAnimatedIcon component to reactively handle changes in the user's motion preference, pausing animations when reduced motion is enabled.
  • Updated the World Cup logo SVG to include a new animation class for counter-rotation, enhancing visual dynamics.
  • Adjusted the fetch URL for the World Cup logo to ensure the latest version is used.
  • Added a feature flag for the referral program in the environment configuration.
  • Updated middleware to bypass authentication for referral routes when the feature is disabled.
  • Integrated referral feature checks across various components and API routes to conditionally render referral UI and handle requests.
  • Enhanced type definitions to include referral program status for better configuration management.
  • refactor(referral): streamline referral feature integration and error handling
  • Updated middleware to return a 404 response for referral routes when the feature is disabled, enhancing user feedback.
  • Introduced a new utility function, isReferralEnabled, to centralize feature flag checks across components and API routes.
  • Removed the deprecated referralFeature module, consolidating referral logic within the utils for better maintainability.
  • Adjusted environment configuration to ensure consistent handling of the referral feature flag.
  • refactor(referral): rename environment variable for referral feature flag

  • fix(referral): enhance error handling in referrer unlock process
  • Updated the ReferrerUnlockResult type to include error handling for profile fetch and verification failures.
  • Modified the checkReferrerUnlock function to return specific error codes and messages for better clarity on failure reasons.
  • Adjusted the tryClaimOne function to handle transient verification failures distinctly from genuine unlock status, improving user feedback.
  • Introduced normalizeStarknetAddressOrNull to handle null or undefined addresses gracefully.
  • Updated ExportStarknetWalletModal to utilize the new function for address normalization, improving address handling and user feedback.
  • Refactored StarknetContext to normalize wallet addresses upon retrieval, ensuring consistent address formatting across the application.
  • refactor(utils): remove normalizeStarknetAddressOrNull and update address normalization in components

  • feat(kyc): implement unlimited monthly limit for tier 3 KYC users

  • Updated KYC tier configuration to allow tier 3 users to have an unlimited monthly spend limit by accepting the "unlimited" sentinel in environment variables.
  • Modified KYC-related components to display "Unlimited" for tier 3 users when applicable.
  • Refactored limit-checking logic in the backend to accommodate the new unlimited tier, ensuring proper handling of transactions without caps.
  • Added database migration to support the new unlimited limit functionality.
  • Implemented a new method to build the full country list using libphonenumber-js, eliminating the need for a network call due to CORS issues with the previous REST Countries API.
  • Updated fetchCountries function to utilize the local country data, ensuring a consistent and reliable list of countries with calling codes and flags.
  • Enhanced phone validation logic to correctly handle country calling codes for regions sharing the same code, specifically for the US and Canada.
  • Introduced formatRecipientNameFirstWordForPill to extract and title-case the first word of a recipient's name.
  • Updated TransactionStatus component to utilize the new formatting function for improved display of recipient names.
  • refactor(wallet): streamline state reset in ExportStarknetWalletModal
  • Introduced a resetExportState function to encapsulate the logic for resetting modal state variables.
  • Updated the useEffect hook to simplify the closing behavior of the modal.
  • Enhanced the AnimatePresence component to call resetExportState on exit completion for better state management.

Replace the "On-ramp"/"Off-ramp" toggle labels in the swap modal with "Buy"/"Sell", and drop the " ~ 1 {token}" suffix from the preview rate.

  • fix(wallet): restore Starknet localStorage cleanup on logout

Clear cached Starknet wallet keys on logout (regression from KYC #496). Also widen Buy/Sell toggle padding from px-3 to px-4.


Description

Describe the purpose of this PR along with any background information and the impacts of the proposed change. For the benefit of the community, please do not assume prior context.

Provide details that support your chosen implementation, including: breaking changes, alternatives considered, changes to the API, contracts etc.

References

Include any links supporting this change such as a:

If there are no references, simply delete this section.

Testing

Describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this project has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors.

Please include any manual steps for testing end-to-end or functionality not covered by unit/integration tests.

Also include details of the environment this PR was developed in (language/platform/browser version).

  • 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

  • New Features
    • Added phone verification, KYC status checks, referral flows, and Starknet Earn deposit/withdraw support.
    • Introduced new wallet, referral, and Earn screens/modals for a richer mobile experience.
  • Bug Fixes
    • Improved wallet, transaction, and order handling for more reliable status updates and validation.
    • Tightened address, phone, and KYC input checks to reduce errors.
  • Documentation
    • Expanded environment setup guidance and added policy pages for KYC and privacy.
  • Chores
    • Added CI workflow and updated ownership/review settings.

* fix(wallet): export Starknet embedded key when network is Starknet (#498)

* feat(wallet): implement Starknet wallet export functionality with HPKE encryption

- Added a new API endpoint for exporting Starknet embedded wallets, utilizing Privy's HPKE encryption.
- Introduced a modal for user interaction during the export process, allowing users to copy their private key securely.
- Updated context and hooks to manage the export modal state and integrate with the existing Starknet provider.
- Added necessary dependencies for HPKE encryption and decryption processes.

This feature enhances the wallet's functionality by enabling secure export of private keys for Starknet users.

* refactor(wallet): update authorization signature handling in Starknet export route

- Replaced the previous authorization signature generation method with a new approach using  and .
- Enhanced error handling for signature generation failures, returning a 500 status with an appropriate error message.
- Cleaned up the code for better readability and maintainability by consolidating signature logic into a single section.

* feat(wallet): enhance ExportStarknetWalletModal with key verification

- Added expectedPublicKey prop to verify the exported key against the wallet's signer key.
- Implemented key verification logic to warn users if the exported key does not match the expected public key or if the derived address differs from the displayed address.
- Introduced a key verification warning message in the modal to inform users of potential mismatches.
- Updated context to pass the publicKey to the ExportStarknetWalletModal for verification purposes.

* feat(kyc): extract tiered KYC flow into clean branch (#496)

* feat(kyc): extract tiered KYC flow into clean branch

Port the tiered KYC API, UI, and migration changes onto latest main while preserving current main exports and dependency baseline to keep the PR isolated and reviewable.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(build): lazy Supabase admin client and restore Starknet providers

- Defer Supabase client creation until first use so next build does not require secrets at bundle time.\n- Restore HomeTransactionFormModeProvider and StarknetProvider around KYC context tree.\n- Add https-proxy-agent for Twilio server external resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(env): update Supabase keys and improve KYC handling

- Revised .env.example to clarify the use of Supabase secret keys and added comments for better guidance.
- Updated jest.setup.js to use the new SUPABASE_SECRET_KEY for testing.
- Enhanced KYC API routes to improve error handling and wallet address verification.
- Introduced new utility functions for KYC tier management and refined transaction handling logic.
- Improved user feedback in KYC and phone verification modals for better clarity and user experience.

* feat(swap): implement swap transaction precheck and enhance KYC handling

- Added a new endpoint for prechecking swap transactions to verify monthly KYC limits without inserting records.
- Introduced utility functions for swap transaction limit checks, improving error handling and user feedback.
- Updated KYC-related API routes to enhance wallet address verification and error messaging.
- Enhanced the .env.example file with additional configuration options for Dojah utility bill processing.
- Improved handling of KYC document uploads and error messages in the KYC verification process.
- Updated transaction form to support new swap functionality and ensure accurate limit checks based on user KYC tier.

* feat(api): enhance transaction handling and KYC validation

- Updated transaction-related API endpoints to support both onramp and offramp transaction types.
- Improved KYC handling by integrating new validation checks for phone numbers and ID information.
- Refactored transaction limit checks to accommodate new transaction types and ensure accurate KYC compliance.
- Enhanced error handling and user feedback in phone verification and KYC modals.
- Added support for keyboard search and filtering in dropdown components for better user experience.
- Updated configuration for server external packages to resolve dependency issues.

* feat(kyc): enhance KYC validation and transaction handling

- Updated .env.example with clearer comments for Smile Identity configuration.
- Modified submitSmileIDData function to include wallet address in headers for KYC submissions.
- Improved error handling in KYC routes to provide more specific feedback for verification attempts.
- Refactored transaction processing to support new transaction types and ensure accurate KYC compliance.
- Enhanced transaction status checks to include new error messages and validation for onramp/offramp transactions.
- Updated database migrations to accommodate changes in transaction types and KYC profile management.

* refactor(transactions): normalize transaction type handling and enhance Twilio verification timeout

- Updated transaction processing to use normalized transaction types for consistency.
- Introduced timeout handling for Twilio verification requests to prevent hanging operations.
- Improved error logging for Twilio verification failures, distinguishing between timeout and other errors.
- Adjusted database migration to enforce unique constraints on transaction records more effectively.

* refactor(profile): streamline KYC tier handling and improve display logic

- Replaced the previous tier expansion state management with a single state for expanded tier level.
- Updated tier display logic to use a new utility function for consistent tier labeling.
- Enhanced the TransactionLimitModal to reflect the current KYC tier accurately and conditionally render tier information.
- Improved user feedback in the ProfileDrawer and TransactionLimitModal for better clarity on KYC status and upgrade options.

* refactor(profile): update icons and KYC requirements for improved clarity

- Replaced AiPhone01Icon with TbPhoneCall in ProfileDrawer for consistency with other icons.
- Updated KYC tier requirements from "Phone verification" to "Phone number" for clearer user understanding.

* refactor(kyc): update KYC tier limits and improve user messaging

- Adjusted KYC tier monthly limits in .env.example and related documentation for clarity.
- Enhanced user messaging in TransactionLimitModal and other components to reflect updated KYC requirements.
- Renamed tier 0 from "Free" to "Unverified" for better understanding of user status.
- Improved handling of phone verification prompts in the transaction flow.

* fix(kyc): update user messaging for KYC verification prompts

- Changed the prompt from "Start" to "Get started" for improved clarity in the KYC verification process when conditions are not met.

* refactor(kyc): enhance TransactionLimitModal messaging and layout

- Updated the title and messaging in TransactionLimitModal to provide clearer instructions based on the user's KYC upgrade step.
- Improved conditional rendering for user limits and verification prompts, ensuring relevant information is displayed based on the user's verification status.
- Adjusted styling and layout for better visual clarity and user experience.

* feat(profile): add ProfileDrawer and integrate profile access in MobileDropdown and SettingsView

- Introduced ProfileDrawer component for user profile management.
- Updated MobileDropdown to include state management for ProfileDrawer and added an onOpenProfile handler.
- Enhanced SettingsView to provide a button for accessing the profile, improving user navigation and experience.

* fix(css): prevent iOS Safari auto-zoom on form fields

- Added CSS rules to ensure input, select, and textarea elements have a minimum font size of 16px on devices with a max-width of 640px, preventing unwanted zoom behavior in iOS Safari.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: sundayonah <sundayonah94@gmail.com>

* feat(utils): add formatUsdAmount function for formatting USD amounts in KYC UI

- Introduced a new utility function, formatUsdAmount, to format USD amounts with up to 4 decimal places and comma separation.
- Updated ProfileDrawer and TransactionLimitModal components to utilize formatUsdAmount for displaying monthly limits and transaction summaries, enhancing consistency in currency formatting across the application.

* feat(kyc): enhance KYC document handling and UI improvements (#511)

- Added CSS styles to prevent 100vh clipping for mobile in the KYC modal's smart camera component.
- Updated the KYC document upload process to only accept utility bills, removing bank statements and PDF support.
- Refined the formatUsdAmount function to limit decimal places to 2 for better consistency in KYC UI.
- Improved user feedback in KYC modals and transaction forms, ensuring clarity on document requirements and verification status.
- Introduced a new KYCStatusSnapshot interface for synchronous KYC status updates, enhancing the user experience during verification processes.

* feat: implement referral system with API endpoints and UI components (#433)

* feat: implement referral system with API endpoints and UI components

* Added new API routes for submitting and retrieving referral data.
* Introduced referral-related types and utility functions for handling referral codes.
* Created UI components for referral dashboard and call-to-action, enhancing user engagement.
* Updated mobile dropdown and main page content to include referral options and modals.
* Enhanced middleware to support new referral API routes.

* feat: enhance referral system and error handling

* Added optional role property to referral data structure for better clarity.
* Improved clipboard copy functionality with error handling for referral codes and links.
* Updated referral data retrieval to include role information for referrers and referred users.
* Enhanced error handling in the ReferralDashboard and ReferralDashboardView components for better user feedback.

* feat: improve referral API and dashboard functionality

* Refactored referral API routes to utilize a new method for retrieving user IDs from requests.
* Updated referral data retrieval to ensure accurate wallet address handling.
* Enhanced error handling for referral code generation and transaction volume checks.
* Improved user feedback in the ReferralDashboard and ReferralDashboardView components for better user experience.
* Standardized handling of referral amounts to ensure consistency across the application.

* feat: add referral volume and reward configuration to environment and API

* Introduced new environment variables for minimum qualifying volume and reward amount in the referral system.
* Updated types to include referral configuration parameters.
* Refactored referral claim logic to utilize new configuration values for volume checks and reward distribution.
* Enhanced referral data retrieval to reflect user-specific claim statuses and auto-claim functionality for eligible referrals.

* feat: enhance referral program configuration and error handling

* Updated .env.example to better document referral program variables.
* Improved KYC status verification in the referral claim API with enhanced error handling for better user feedback.
* Refactored referral configuration values to ensure proper parsing and validation in the application.

* refactor: rename avatar image utility function and update references

- Renamed `getAvatarImage` to `getAvatarImageFromAddress` for clarity in its purpose.
- Updated all references to the renamed function across components, ensuring consistent usage.
- Added comments to improve code documentation and understanding of avatar image retrieval logic.

* fix(referral): use config reward amount and normalize self-referral check

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(referral): address PR review feedback on modal UX and copy

Remove duplicate submit toast, show referral prompt on login without
requiring network modal, fix unescaped apostrophe, and standardize
qualifying volume copy and env default to $20.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(referral): make submit insert atomic and handle unique conflicts

Remove race-prone preflight read, return 409 on referred_wallet_address
unique violations, and add a case-insensitive unique index migration.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Isaac Onyemaechi <amaechiisaac450@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(kyc): update layout and styling for KYC modal and profile drawer (#512)

* refactor(kyc): update layout and styling for KYC modal and profile drawer

- Adjusted CSS properties for the smart camera component to improve height handling.
- Enhanced KYC modal layout with updated maximum height settings for better responsiveness.
- Modified ProfileDrawer to allow vertical scrolling, ensuring content accessibility without overflow issues.
- Improved styling for the smart camera component within the KYC modal for a more consistent user experience.

* fix(kyc): correct max-height CSS property in KYC modal layout

- Updated the max-height property in the KYC modal to use 'max-h-fit' for improved layout consistency and responsiveness.
- Ensured that the modal maintains a better visual structure when displaying content based on user needs.

---------

Co-authored-by: chibie <chibuotu@gmail.com>

* fix(privy): close getPrivyUserIdFromRequest function to improve code structure (#513)

* feat: enable on-ramp on Starknet. (#504)

* refactor(validation): streamline wallet address validation logic

- Removed Starknet-specific checks from various components and hooks, simplifying the handling of on-ramp conditions.
- Replaced the deprecated `isValidEvmAddressCaseInsensitive` function with a new `validateWalletAddress` function that supports both EVM and Starknet address formats.
- Updated the `RecipientDetailsForm` and `TransactionForm` components to utilize the new validation logic, enhancing code clarity and maintainability.
- Removed unused Starknet-related props and conditions across components to improve overall code efficiency.

* refactor(TransactionForm): update connected wallet address handling

* refactor(wallet): enhance useWalletAddress hook and update TransactionForm

* refactor(TransactionForm): update wallet address validation logic

---------

Co-authored-by: Chibuotu Amadi <chibuotu@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fixes (#515)

* feat(starknet): add Earn product via Vesu lending pools (#499)

* feat(starknet): add Earn product via Vesu lending pools

Adds an "Earn" action in the Starknet wallet drawer that lets users supply
USDC and USDT to Vesu lending pools through the starkzap SDK:

- USDC → Clearstar USDC Reactor pool
- USDT → Prime pool

Surfaces:
- New "Earn" action button in the wallet sidebar (Starknet only).
- Earn modal with deposit/withdraw tabs, token selector, live APR /
  monthly / yearly projection, and a success view mirroring Transfer.
- New "Earn activity" tab next to Balances and Transactions, with a
  per-token "Currently supplied" card and a date-grouped list of past
  deposits/withdrawals. Clicking a row opens a detail page with a
  Voyager link, mirroring the Transactions detail flow.

Integration:
- starkzap SDK (^3.0.0) for Vesu deposit/withdraw call preparation,
  market metadata, and position reads.
- next.config aliases shim out starkzap's optional peer deps so Webpack
  resolves cleanly without bloating node_modules.
- Reuses existing noblocks Privy primitives (getStarknetWallet, rawSign,
  buildReadyAccount, setupPaymaster, deployReadyAccount). No changes to
  wallet creation, auth, or signing pipelines.

Includes scripts/test-starkzap.ts: a read-only integration smoke test
that hits Vesu mainnet (~2s) to verify SDK call shapes and pool wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(starknet/earn): drop client-supplied address, derive server-side

Address reviewer feedback from @0xLucqs on starkience/noblocks#1:
the deposit/withdraw routes accepted a wallet address from the request
body without verifying it matched the one derived by buildReadyAccount.
No attack vector (auth gates the route), but a buggy client could send
a mismatched address and credit a Vesu position to the wrong owner.

Cleanest fix per his suggestion: remove the field. Compute the canonical
address server-side via computeReadyAddress(walletPublicKey, classHash)
using the publicKey fetched from Privy in getStarknetWallet. That value
equals what buildReadyAccount derives, so mismatch is now impossible by
construction. Drops a redundant validateAndParseAddress block and the
unused starknet/validateAndParseAddress import; also drops the unused
deployReadyAccount import in withdraw.

Client (useEarnHandler) no longer sends address in deposit/withdraw
request bodies. UI readiness gate keeps the address check (it gates the
button, not the payload).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(starknet/earn): address CodeRabbit review on PR #499

- deposit/withdraw routes: log a warning when waitForTransaction soft-fails,
  matching the existing pattern in app/api/starknet/transfer/route.ts
  instead of silently swallowing the error
- EarnActivityDetails: add aria-label to icon-only copy button
- EarnActivityPanel: clickable row uses motion.button (with type="button"
  and w-full/text-left/bg-transparent so the layout matches the prior
  motion.div) so the row is keyboard-operable
- earn.ts: log getPositions failures via console.error before falling back
  to the empty positions array, so production debugging can distinguish
  "user has no position" from "fetch failed"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(starknet/earn): honest confirmation flag + lossless Starknet balance

CodeRabbit follow-up on PR #499:

1. Deposit / withdraw responses now include a `confirmed: boolean` field.
   When `waitForTransaction` throws (network error, RPC hiccup), the
   handler still returns `success: true` with the tx hash so the client
   gets the optimistic UX, but `confirmed: false` so the response is
   honest about the on-chain state. The client can ignore the field
   today (preserves current UX) or surface it in a future pass.

2. Starknet wallet balances now carry `balancesInWei: Record<string,
   bigint>` end-to-end:
   - `fetchStarknetBalancesUnified` (app/utils.ts) was already computing
     the per-token bigint internally; it now stores and returns it,
     mirroring the EVM path.
   - `fetchStarknetBalance` wrapper exposes the new field.
   - `WalletBalances` interface (app/context/BalanceContext.tsx) declares
     `balancesInWei?` as an optional field, so existing consumers are
     unaffected.
   - `EarnWalletForm` reads `allBalances.starknetWallet?.balancesInWei?.[token]`
     directly instead of the lossy `parseAmountToBaseUnits(walletBalanceUnit.toString())`
     round-trip. The Max button and amount validation now use exact base
     units, eliminating precision loss at the 6th USDC decimal.

The `WalletBalances` change is backward-compatible (optional field with
a `BigInt("0")` fallback in the consumer); other balance consumers
(Transfer, swap, balance display) are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(balances): preserve balancesInWei end-to-end on EVM and Starknet

CodeRabbit follow-up on PR #499: the previous commit added
`balancesInWei?` to the WalletBalances interface and plumbed it through
the Starknet path, but EVM balances still discarded it in
`buildWalletBalancesFromRaw`. That made the JSDoc claim ("Populated by
Starknet and EVM fetchers") misleading and left a latent precision gap
for any future caller doing exact-integer math on EVM amounts.

Concretely:
- `buildWalletBalancesFromRaw` now accepts an optional `balancesInWei`
  parameter and returns it on the WalletBalances object. The CNGN
  conversion logic is unchanged; base units bypass the float corridor
  entirely (correct, since CNGN <-> NGN is a display-only rate).
- All 7 call sites in BalanceContext.tsx that build WalletBalances from
  a `fetchWalletBalance` result now pass `result.balancesInWei` through.
- The two patch sites in the `[cngnRate]` useEffect (re-applying the
  rate after it resolves) preserve `balancesInWei` from the existing
  WalletBalances object.
- `fetchStarknetBalancesUnified` (utils.ts) populates `balanceWei` on
  each ChainBalanceEntry, matching the EVM entries shape so per-token
  exact-integer math is available downstream.

All changes are backward-compatible: `balancesInWei` is optional on
WalletBalances, and existing consumers that read only `balances`
(display number) are unaffected. The roadmap to bridge EVM USDC into
Vesu via the Earn product can now read `allBalances.<chain>?.balancesInWei?.[token]`
on any network without further infrastructure changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(starknet/earn): return 502 when waitForTransaction confirmation fails

Addresses CodeRabbit's follow-up on PR #499 (after @onahprosper asked
for a recheck). The previous approach (return `success: true,
confirmed: false`) relied on a client-side check that does not exist
in useEarnHandler, so withdrawals where on-chain confirmation failed
were still being recorded as completed activity in the UI.

Switching to CodeRabbit's original suggestion of an HTTP 502 response:
the failure is now honest at the protocol level, the existing client
check (`!res.ok || !data?.success`) treats it correctly without any
client change, and the response body still carries `transactionHash`
so callers can point the user to the explorer.

Drops the now-unused `confirmed` flag from both deposit and withdraw
response shapes. Same simplification applied to both routes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(starknet/earn): drive token list from Paycrest aggregator

Addresses @onahprosper's review feedback on PR #499: replaces the
hard-coded EARN_TOKENS dropdown with a runtime fetch of Paycrest's
supported-token list (via the existing `getNetworkTokens("Starknet")`
helper, which already wraps `api.paycrest.io/v2/tokens` with cache +
fallback).

New `useEarnAvailableTokens()` hook in useEarnHandler.ts:

- Fetches Paycrest's Starknet token list on mount.
- Intersects with `EARN_TOKENS` (the registry of tokens we have Vesu
  pool mappings for) so we never expose a token without a pool.
- Falls back to the full `EARN_TOKENS` list if Paycrest is unreachable
  or returns no Starknet tokens.

EarnWalletForm.tsx now consumes the hook for both the dropdown items
and the `onSelect` handler (the previous `name === "USDT" ? ... : ...`
hardcode is replaced with a lookup against the dynamic list).

Effect: when Paycrest adds a new Starknet token AND we add a matching
Vesu pool mapping to `EARN_TOKEN_CONFIG`, the new token appears in
the UI with no further code change. The `EARN_TOKENS` registry remains
the source of truth for which tokens our backend can build calls for;
it is kept static (and exported) for type-level coverage and for the
position-refresh / activity-filter loops that should iterate every
supported token regardless of Paycrest's current advertisement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update app/api/starknet/earn/deposit/route.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(starknet): extract applySafetyMargin to shared utility; gate fee estimation on isDeployed

Addresses two CodeRabbit comments on PR #499:

1. Fee estimation runs on undeployed accounts without deployment context, and
   the result is discarded on the deploy path.

   In `app/api/starknet/earn/deposit/route.ts`, the fee-estimation block now
   only runs when `isDeployed && !isSponsored`. On the first-deposit path
   (account not yet deployed), `deployReadyAccount` already builds its own
   `deploy_and_invoke` paymaster transaction with proper `deploymentData`
   and performs its own deployment-aware fee estimation internally, so
   estimating here without `deploymentData` produced an inaccurate fee that
   was never used. Withdraw is unaffected because it already rejects
   undeployed accounts upfront.

2. `withMargin15` was misleadingly named and duplicated across 5 sites.

   The helper actually applies a 1.5x (50%) safety margin via
   `(bi * 3 + 1) / 2`, not 15%. Extracted into a single shared
   `applySafetyMargin(v)` export in `app/lib/starknet.ts` with a clarifying
   doc comment. All 5 call sites updated to import and use it:

   - `app/lib/starknet.ts:deployReadyAccount`
   - `app/api/starknet/transfer/route.ts`
   - `app/api/starknet/create-order/route.ts`
   - `app/api/starknet/earn/deposit/route.ts`
   - `app/api/starknet/earn/withdraw/route.ts`

   The local duplicate copies are removed; the formula is unchanged. This
   keeps the cross-route fee-margin behaviour consistent and gives the
   helper an honest name that matches the math.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(earn): implement Earn feature with consent modal and UI components

- Introduced EarnConsentModal to handle user consent for using the Earn feature.
- Updated MobileDropdown and WalletDetails components to integrate Earn UI and manage user interactions.
- Enhanced state management for Earn activities and consent handling through useEarnAccess hook.
- Configured environment variables for Earn feature toggling.

* fix(earn): update EarnConsentModal risk copy and enhance MobileDropdown and WalletDetails components

* fix(earn): update risk copy in EarnConsentModal and enhance balance checks in EarnWalletForm and EarnHubView components

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Prosper <40717516+onahprosper@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Isaac Onyemaechi <amaechiisaac450@gmail.com>
Co-authored-by: Chibuotu Amadi <chibuotu@gmail.com>

* feat(kyc): enhance SmileID verification process and error handling (#516)

* feat(kyc): enhance SmileID verification process and error handling

- Introduced a retry mechanism for SmileID job submissions when database-related failures occur, improving user experience during backend outages.
- Added a classification function to categorize SmileID verification failures, providing clearer feedback on the nature of errors.
- Updated KycModal to display specific failure reasons and hints based on the error type, enhancing user guidance during document verification.
- Improved error handling in KycModal to set and display failure messages more effectively, ensuring users receive relevant information for troubleshooting.

* refactor(kyc): improve error classification and user feedback in SmileID verification

- Enhanced the classifySmileIdFailure function to better categorize database-related errors, improving clarity on failure reasons.
- Updated KycModal to provide more specific failure hints based on error types, enhancing user guidance during verification.
- Adjusted messaging for verification failures to ensure users receive relevant and actionable feedback.

* fix(kyc): improve attempt counter restoration for SmileID verification failures

- Updated logic to restore the attempt counter for users experiencing infrastructure outages, regardless of job type.
- Enhanced error handling to log failures when restoring the attempt counter, providing better visibility into issues during SmileID verification.

* feat(referrals): update referral components and replace ReferralDashboard with ReferralHubView (#517)

- Added support for "swap" transaction type in getTransactionHistoryTypeLabel function.
- Replaced ReferralDashboard with ReferralHubView in MobileDropdown and WalletDetails components for improved referral management.
- Removed unused ReferralDashboard and ReferralDashboardViewSkeleton components to streamline the codebase.
- Enhanced ReferralCTA to directly trigger the new referral view, improving user navigation.

* fix(wallet): restore network-aware address in settings dropdown

The desktop settings dropdown dropped the useWalletAddress() hook (KYC #496)
and inlined EVM-only logic, so on Starknet it showed the EVM smart-wallet
address instead of the Starknet Ready account. Restore the hook (which already
handles the Starknet branch) with the migration-based EVM fallback, matching
WalletDetails and MobileDropdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(referral): gate modal on server-side referral status to prevent cross-device reappearance and delayed load (#518)

* fix(referral): gate modal on server-side referral status to prevent cross-device reappearance and delayed load

* address review comments

* address review comments

* fix(ui): show correct address and networks in wallet copy modal (#519)

* fix(ui): show correct address and networks in wallet copy modal

- Replace manual EVM-only walletAddress derivation in SettingsDropdown
  with useWalletAddress() hook so the Starknet address is shown/copied
  when on Starknet network
- Filter Starknet out of supportedNetworksDisplayed in
  CopyAddressWarningModal when the active network is EVM, so Starknet
  is never listed as a deposit option for an EVM address
- Restore scroll position after KYC "Let's go!" closes the dialog —
  SmileID's camera component displaces document.scrollY; a
  requestAnimationFrame scrollTo(0) fires after Headless UI unlocks scroll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(settings): update wallet address logic to handle Starknet network

- Introduced useNetwork context to determine the selected network.
- Adjusted wallet address derivation to ensure that an EVM address is not shown when on the Starknet network, improving user experience and accuracy in address display.
- Simplified fallback logic for wallet address to prioritize network-aware handling.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* updated privacy policy and terms (#509)

* updated privacy policy and terms

* feat(kyc): add KYC policy page and update routing

- Introduced a new KYC policy page with comprehensive details on data handling and user privacy.
- Updated the main content to include the new KYC policy route.
- Enhanced the sitemap to include the KYC policy URL for better SEO.
- Modified the KycModal to link to the new KYC policy instead of the privacy policy.
- Added a CI workflow for continuous integration and testing on main and stable branches.

* feat(ci): configure environment variables for CI workflow

- Added environment variables for various services including database, API keys, and URLs to the CI workflow.
- Included dummy values for build-time variables that are not stored in repository secrets to ensure successful builds.

* fix(ci): update Privy SDK environment variables in CI workflow

- Changed dummy values for NEXT_PUBLIC_PRIVY_APP_ID and PRIVY_JWKS_URL to valid placeholders required by the Privy SDK.
- Ensured that the CI workflow is correctly configured for integration with the Privy service.

* refactor(kyc): update KYC verification process and improve UI components (#520)

* refactor(kyc): update KYC verification process and improve UI components

- Replaced the KYC status check with a direct query to Supabase for user verification, enhancing reliability.
- Updated error handling to provide clearer messages regarding KYC requirements.
- Refactored the CopyAddressWarningModal and EarnConsentModal components to improve checkbox styling and accessibility.
- Enhanced the KycModal to restore scroll position after closing, ensuring a smoother user experience.
- Added a useEffect in MobileDropdown to reset the view state when the dropdown closes, improving navigation consistency.

* refactor(ui): enhance checkbox accessibility and scroll restoration in modals

- Updated checkbox styling in CopyAddressWarningModal for improved focus visibility and accessibility.
- Introduced a new function in KycModal to restore scroll position after closing, enhancing user experience during modal transitions.

* feat(MobileDropdown): reset nested views on sheet close

- Added a useEffect hook to reset the current view to "wallet" and clear selected activities when the dropdown sheet closes. This ensures a consistent starting point for users when reopening the dropdown.

* refactor(referral): enhance claim qualification logic and error handling (#522)

* refactor(referral): enhance claim qualification logic and error handling

- Introduced a new `checkPartyQualification` function to streamline KYC and volume checks for both claimants and referees.
- Updated error responses to provide clearer messages based on the qualification subject (claimant or referee).
- Refactored the `tryClaimOne` function to utilize the new qualification checks, improving code readability and maintainability.
- Adjusted comments and documentation to reflect the changes in the claim process and qualification requirements.

* refactor(referral): improve error handling and qualification checks in claim process

- Enhanced the `checkPartyQualification` function to include timeout handling and improved error messages for failed rate lookups.
- Updated the `tryClaimOne` function to streamline qualification checks and provide clearer error responses when verification fails.
- Refactored the logic to ensure that invalid NGN rate payloads are properly handled, improving overall robustness of the claim process.
- Adjusted comments for better clarity on the qualification checks and their implications in the claim workflow.

---------

Co-authored-by: Isaac Onyemaechi <amaechiisaac450@gmail.com>

* refactor(MobileDropdown): streamline component structure and improve view handling

- Simplified the rendering logic within the MobileDropdown component by removing unnecessary motion animations and restructuring the JSX for better readability.
- Enhanced the handling of different views (wallet, referrals, earn) to ensure consistent overflow behavior and improved user experience.
- Updated the DialogPanel and view components to maintain a cleaner layout and better manage state transitions.

* refactor(TransferForm): improve recipient address validation and error handling

- Added `dirtyFields` to form state to track changes in the recipient address field.
- Introduced a new `showRecipientAddressError` variable to conditionally display error messages based on the recipient address's validity and its dirty state.
- Updated the `useEffect` hook to trigger validation only when the recipient address is not empty, enhancing performance and user experience.

* Audit of the entire kYC | Earn | Referral flow (#524)

* fix(kyc): reliable scroll restore and fresh tier status after verification

- Move scroll restoration out of KycModal's self-cancelling 500ms timer into
  AnimatedModal via AnimatePresence onExitComplete (opt-in restoreScrollOnClose).
  The old timer was cleared by the component's own unmount cleanup, racing the
  exit animation, and only covered two of the modal's close paths.
- Restore the scroll position captured at open instead of jumping to top.
- Remove the outer AnimatePresence/conditional around the KycModal mounts so the
  exit animation (and scroll restore) actually runs; drop TransactionLimitModal's
  early return-null for the same reason.
- Force-refresh KYC status when ProfileDrawer and TransactionLimitModal open so
  a tier upgrade landing via the SmileID webhook is not hidden by the context's
  30s staleness cache.
- Sync KYCContext when KycModal's poller detects the tier upgrade (previously
  the modal could show success while the rest of the app showed the old tier).

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* fix(kyc): enforce one verified identity per phone/ID and harden SmileID callback

- Add pending_phone_number staging: send-otp no longer overwrites the verified
  phone_number (or de-verifies the profile); verify-otp promotes the pending
  number only after the OTP is confirmed. Previously a user who started (but
  never finished) verifying a replacement number kept their tier and the API
  reported the unverified number as verified.
- Enforce one verified profile per phone number (send-otp pre-check, verify-otp
  promotion guard) and per ID document (smile-id route), with partial unique
  indexes created by migration when no historical duplicates exist. Previously
  the same phone/ID could verify unlimited wallets, multiplying monthly limits.
- Harden the SmileID callback: reject stale/unparseable timestamps (the HMAC
  only covers timestamp+partner_id, so captured signatures replay forever) and
  confirm the job outcome via SmileID's signed job_status API before any tier
  promotion instead of trusting unsigned body fields.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* fix(referral,earn): correct modal gating, pending totals, and per-user earn consent

Referral modal:
- Suppress for ANY referral relationship, not just role === 'referred', so
  existing users who have referred others no longer see the new-user modal.
- Require a known account age within 30 days; previously a missing createdAt
  skipped the new-user check entirely and let existing accounts through.

Network selector (root cause of 'referral popup only after refresh'): gate the
open trigger on Privy 'ready' and re-check once the embedded wallet address
settles, so a fresh signup reliably opens the network modal — the referral
modal chains off its close callback.

Referral Pending/Earned cards: coerce reward_amount to Number in the API totals
(Postgres numeric deserializes as string via PostgREST, so the reduce was
string-concatenating and corrupting total_pending), and harden the card
formatting against non-numeric values.

Earn disclaimer: key the 'seen' flag per Privy user id instead of a single
device-global key, so a second user on the same device (and brand-new signups)
see the risk disclosure instead of inheriting the first user's acceptance.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* fix: verification-pass corrections for KYC and network-modal gating

- smile-id route: keep idNumberToStore undefined (not null) when SmileID
  returns no ID number, so the update no longer nulls out a previously stored
  id_number (regression introduced with the uniqueness check).
- smile-id callback: accept SmileID's string 'true' job flags when confirming
  job_status, matching how the rest of the codebase treats these fields.
- Network modal dismissal: wire the never-called markNetworkModalDismissed()
  into NetworkSelectionModal.handleClose, and centralize the hasSeenNetworkModal
  key in networkModalStore with a canonical lowercased form (legacy checksummed
  keys still honored on read). Previously MigrationBannerWrapper's two gates
  were both dead — the live store signal never fired and the storage fallback
  lowercased a key written checksummed — so migration UI gated on network-modal
  dismissal could not appear in-session, and the fresh-signup reset in Navbar
  removed a key form that was never written.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* fix: update phone number placeholder text for consistency

- Changed the placeholder text in the PhoneVerificationModal from "enter your phone number" to "Enter your phone number" to ensure consistent capitalization and improve user experience.

* fix: address CodeRabbit review on PR #524

- verify-otp: gate phone promotion on the current pending_phone_number and
  treat zero updated rows as a superseded verification, so a stale OTP can no
  longer promote an old number over a newer pending request.
- NetworkSelectionModal: replace the sticky hasCheckedStorage boolean with a
  wallet-scoped sentinel so eligibility re-evaluates after logout/login or a
  wallet switch.
- networkModalStore: wrap localStorage access in try/catch helpers so
  restricted-storage environments degrade gracefully instead of throwing.
- smileID: only accept 0/1 or an http(s) URL for SMILE_IDENTITY_SERVER(_MODE);
  unrecognized values now surface the configuration error instead of silently
  defaulting to sandbox.
- smile-id callback: the unsigned body is now used for routing only — the job
  outcome AND the full_name/date_of_birth enrichment come exclusively from
  SmileID's signed job_status response.
- migration: RAISE EXCEPTION (fail deploy) when historical duplicate verified
  identities exist, instead of warning and skipping the unique indexes.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* fix: update phone number placeholder text for consistency

- Changed the placeholder text in the PhoneVerificationModal from "enter your phone number" to "Enter your phone number" to ensure consistent capitalization and improve user experience.

* fix: update Earn disclosure URL for improved accessibility

- Changed the Earn risk disclosure article link from a Google Document to a dedicated blog page on noblocks.xyz, enhancing user access to important information.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* fix(ux): stop network modal re-opening on login and pin body scroll behind locked modals

- NetworkSelectionModal: pass the raw wallet address into
  hasSeenNetworkModalFlag — pre-lowercasing it defeated the legacy
  checksummed-key fallback, so the modal re-opened on login for every
  pre-existing account (regression from the wallet-scoped sentinel fix).
  Only the sentinel comparison is case-normalized now.
- AnimatedModal: replace restore-scroll-on-close with a body scroll lock
  (position: fixed at the captured offset) held from open through the end of
  the exit animation. The document can no longer be dragged to the bottom by
  modal content (e.g. the SmileID camera), and the visible jump from the old
  after-the-fact scrollTo restore is gone — the page never moves. Refcounted
  so overlapping locked modals share one pin; released on exit complete with
  an unmount safety net. Prop renamed restoreScrollOnClose -> lockBodyScroll.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* Update CODEOWNERS to include additional reviewers for pull requests (#525)

* Update CODEOWNERS to include additional reviewers for pull requests

* fix: apply CodeRabbit auto-fixes

Fixed 2 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* fix(referral): carry ?ref= code from share links into the referral modal

Share links are generated as ?ref=NBXXXX (handleCopyLink) but nothing ever
consumed the parameter — the only reader of ref was the BlockFest hook — so
users arriving via a referral link still had to type the code manually.

- Capture ?ref= on landing in MainPageContent (before login, since OAuth can
  drop the query string) and persist it as a pending code in localStorage.
- Pre-fill the ReferralInputModal input from the pending code when it opens,
  and re-open the modal for a link-carried code even if this wallet dismissed
  it before (clicking a referral link is explicit intent).
- Clear the pending code on successful submit and on explicit skip/backdrop
  close so a declined code cannot keep re-triggering the modal.
- Shared format guard (NB + 4 alphanumerics) keeps BlockFest's ?ref=blockfest
  flow untouched and replaces the inline regex in the modal.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* fix: address CodeRabbit review on PR #526

- AnimatedModal body lock: snapshot pre-existing inline body styles on first
  acquire and restore them on final release instead of blanking them; acquire
  the lock in an isomorphic layout effect so the pin lands before the open
  modal's first paint (no gap for mount-time autofocus/camera scroll).
- networkModalStore: match the hasSeenNetworkModal key case-insensitively on
  read and clear, so legacy keys in any historical address casing are honored
  regardless of how Privy cases the address across hydration/login.

https://claude.ai/code/session_012WaogE9qdJ86chMiHNGPei

* fix(kyc): update transaction status filters to accurately reflect monthly spend limits

- Adjusted the status filters for offramp and onramp transactions to include 'pending', 'fulfilling', 'fulfilled', and 'completed' statuses, ensuring that all relevant transactions contribute to the monthly spend limit.
- Updated the SQL migration to relax the transactions.status CHECK constraint, allowing for accurate status updates without constraint violations.
- Enhanced the logic in the transaction summary API to align with the new status definitions, improving the accuracy of spend calculations.

* refactor(kyc, referral): normalize currency casing and improve transaction filtering

- Introduced a normalization function for currency values to ensure consistent casing across different sources, addressing discrepancies between "cNGN" and "CNGN".
- Updated transaction filtering logic in both the KYC transaction summary and referral claim APIs to utilize Sets for improved performance and clarity.
- Adjusted transaction processing to apply normalization when handling currency values, enhancing the accuracy of USD calculations and overall transaction integrity.

* feat(login): implement body scroll pinning during Privy login flow

- Introduced a new hook, useLoginWithScrollPin, to manage body scroll locking while the Privy login dialog is active, preventing unwanted scrolling on mobile devices.
- Updated AnimatedModal to default lockBodyScroll to true, ensuring modals maintain scroll lock by default.
- Refactored Navbar and TransactionForm components to utilize the new loginWithScrollPin function for improved user experience during login.

* fix(login): enhance login flow with in-flight guard and error handling

- Added an in-flight guard to prevent multiple rapid login attempts while the pin is held.
- Implemented error handling during the login process to ensure proper release of the scroll lock in case of failures.

* Referrers should only require one qualifying transaction

* Replace Noblocks logos in Navbar with SVG image and adjust dropdown arrow position. Add new SVG logo file for Noblocks World Cup logo.

* feat(kyc): enhance KYC status and phone verification flow

- Updated the KYC status API to include the user's full name in the response.
- Modified the PhoneVerificationModal to conditionally display a full name input field based on the user's existing data.
- Improved user prompts and information displayed during the phone verification process to enhance clarity and user experience.
- Adjusted KYC context to manage full name state alongside other KYC details.

* feat(icons): add animated Noblocks logo and World Cup assets

- Introduced a new animated icon component that cycles through three states: the default Noblocks "n" icon, a spinning soccer ball, and the World Cup trophy.
- Created separate components for the Noblocks World Cup logo and the animated icon, ensuring cross-browser compatibility by injecting SVGs directly into the DOM.
- Updated the Navbar to utilize the new animated logo and World Cup logo components, enhancing the visual appeal and user experience.
- Added CSS animations for the spinning effect and implemented reduced motion preferences for accessibility.

* refactor(icons): enhance NoblocksAnimatedIcon for reduced motion support and update World Cup logo SVG

- Improved the NoblocksAnimatedIcon component to reactively handle changes in the user's motion preference, pausing animations when reduced motion is enabled.
- Updated the World Cup logo SVG to include a new animation class for counter-rotation, enhancing visual dynamics.
- Adjusted the fetch URL for the World Cup logo to ensure the latest version is used.

* feat(referral): implement referral program feature flag and UI integration (#531)

* feat(referral): implement referral program feature flag and UI integration

- Added a feature flag for the referral program in the environment configuration.
- Updated middleware to bypass authentication for referral routes when the feature is disabled.
- Integrated referral feature checks across various components and API routes to conditionally render referral UI and handle requests.
- Enhanced type definitions to include referral program status for better configuration management.

* refactor(referral): streamline referral feature integration and error handling

- Updated middleware to return a 404 response for referral routes when the feature is disabled, enhancing user feedback.
- Introduced a new utility function, isReferralEnabled, to centralize feature flag checks across components and API routes.
- Removed the deprecated referralFeature module, consolidating referral logic within the utils for better maintainability.
- Adjusted environment configuration to ensure consistent handling of the referral feature flag.

* refactor(referral): rename environment variable for referral feature flag

---------

Co-authored-by: chibie <chibuotu@gmail.com>

* fix(referral): enhance error handling in referrer unlock process

- Updated the ReferrerUnlockResult type to include error handling for profile fetch and verification failures.
- Modified the checkReferrerUnlock function to return specific error codes and messages for better clarity on failure reasons.
- Adjusted the tryClaimOne function to handle transient verification failures distinctly from genuine unlock status, improving user feedback.

* feat(utils): add normalizeStarknetAddressOrNull function and update E… (#538)

* feat(utils): add normalizeStarknetAddressOrNull function and update ExportStarknetWalletModal and StarknetContext to use it

- Introduced normalizeStarknetAddressOrNull to handle null or undefined addresses gracefully.
- Updated ExportStarknetWalletModal to utilize the new function for address normalization, improving address handling and user feedback.
- Refactored StarknetContext to normalize wallet addresses upon retrieval, ensuring consistent address formatting across the application.

* refactor(utils): remove normalizeStarknetAddressOrNull and update address normalization in components

* feat(kyc): implement unlimited monthly limit for tier 3 KYC users

- Updated KYC tier configuration to allow tier 3 users to have an unlimited monthly spend limit by accepting the "unlimited" sentinel in environment variables.
- Modified KYC-related components to display "Unlimited" for tier 3 users when applicable.
- Refactored limit-checking logic in the backend to accommodate the new unlimited tier, ensuring proper handling of transactions without caps.
- Added database migration to support the new unlimited limit functionality.

* feat(countries): replace REST API with local country data from libphonenumber-js (#540)

- Implemented a new method to build the full country list using libphonenumber-js, eliminating the need for a network call due to CORS issues with the previous REST Countries API.
- Updated fetchCountries function to utilize the local country data, ensuring a consistent and reliable list of countries with calling codes and flags.
- Enhanced phone validation logic to correctly handle country calling codes for regions sharing the same code, specifically for the US and Canada.

* feat(utils): add function to format first word of recipient name in t… (#500)

* feat(utils): add function to format first word of recipient name in title case

- Introduced `formatRecipientNameFirstWordForPill` to extract and title-case the first word of a recipient's name.
- Updated `TransactionStatus` component to utilize the new formatting function for improved display of recipient names.

* refactor(wallet): streamline state reset in ExportStarknetWalletModal

- Introduced a `resetExportState` function to encapsulate the logic for resetting modal state variables.
- Updated the `useEffect` hook to simplify the closing behavior of the modal.
- Enhanced the `AnimatePresence` component to call `resetExportState` on exit completion for better state management.

---------

Co-authored-by: chibie <chibuotu@gmail.com>

* fix(swap): rename ramp toggle to Buy/Sell and trim rate suffix (#514)

Replace the "On-ramp"/"Off-ramp" toggle labels in the swap modal with
"Buy"/"Sell", and drop the " ~ 1 {token}" suffix from the preview rate.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(wallet): restore Starknet localStorage cleanup on logout

Clear cached Starknet wallet keys on logout (regression from KYC #496).
Also widen Buy/Sell toggle padding from px-3 to px-4.

---------

Co-authored-by: chibie <chibuotu@gmail.com>
Co-authored-by: Prosper <40717516+onahprosper@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: sundayonah <sundayonah94@gmail.com>
Co-authored-by: Onah Prosper <prosperauthor@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Starkience <129275548+starkience@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces tiered KYC verification (phone OTP, SmileID Tier 2, Dojah Tier 3), a Starknet Earn (Vesu) lending feature, a referral program, and refactors aggregator/transaction/order API routes with wallet-JWT authorization. It also updates navbar branding, wallet sidebar UI, CI workflow, and env configuration.

Changes

Config, CI & Shared Helpers

Layer / File(s) Summary
Env vars, CODEOWNERS, CI
.env.example, .github/CODEOWNERS, .github/workflows/ci.yml
Adds KYC/phone/Earn/referral env vars, expands CODEOWNERS, and adds a new CI workflow running lint/build.
Feature flags & shared helpers
app/lib/config.ts, app/lib/earnFeature.ts, app/lib/kyc-tier-limits.ts, app/lib/kyc-upgrade-path.ts, app/lib/validation.ts, app/lib/starknet.ts, app/lib/supabase.ts, app/lib/privy.ts, app/lib/_starkzap-unused-shim.ts, app/mainContent.tsx
Adds earn/referral feature flags, tier limit parsing, applySafetyMargin, Starknet address validation, lazy Supabase admin client, and Privy JWT/linked-address helpers.

API Refactor

Layer / File(s) Summary
Order-id parsing
app/lib/payment-order-id.ts
Adds gateway/sender order-id detection and chain-id/network mapping.
Wallet/JWT authorization
app/lib/transaction-wallet-auth.ts, app/lib/swap-transaction-limit-server.ts
Adds wallet ownership verification and KYC-based swap/onramp limit checks.
Aggregator client rewrite
app/api/aggregator.ts
Switches to relative /api/v1 routes, adds V2 order mapping, precheck, SmileID submission, and referral API calls.
Order/transaction routes
app/api/blockfest/cashback/route.ts, app/api/v1/payment-orders/[id]/route.ts, app/api/v1/transactions/*, app/api/internal/set-wallet-context/route.ts
Uses new order-id/wallet-auth helpers for lookup, authorization, and swap-limit precheck.
Frontend reconciliation
app/context/TransactionsContext.tsx
Updates fetchOrderDetails call and ignores 404s during reconciliation.

Tiered KYC

Layer / File(s) Summary
Phone libraries
app/lib/phone-validation.ts, app/lib/countries.ts, app/lib/phone-verification.ts
Adds phone parsing/validation and KudiSMS/Twilio provider logic.
Phone OTP routes & modal
app/api/phone/send-otp/route.ts, app/api/phone/verify-otp/route.ts, app/components/PhoneVerificationModal.tsx
Implements OTP send/verify flow and its UI.
SmileID KYC
app/lib/smileID.ts, app/lib/smileIdIdValidation.ts, app/lib/smileCameraTheme.ts, app/api/kyc/smile-id/*
Implements Enhanced/Biometric KYC submission and callback confirmation.
Dojah Tier 3
app/lib/dojah.ts, app/api/kyc/tier3-verify/route.ts
Implements utility-bill address verification and Tier 3 upgrade.
KYC status/limits
app/api/kyc/status/route.ts, app/api/kyc/transaction-summary/route.ts, app/context/KYCContext.tsx
Exposes tier/limit state via new API routes and React context.
KYC UI
app/components/KycModal.tsx, TransactionLimitModal.tsx, ProfileDrawer.tsx, kyc/DocumentRequirementsModal.tsx, policy pages, SettingsDropdown.tsx
Rewrites KYC modal flow and adds profile/limit UI and policy pages.

Starknet Earn

Layer / File(s) Summary
Earn library & hooks
app/lib/earn.ts, earnConsent.ts, useEarnAccess.ts, useEarnHandler.ts
Vesu deposit/withdraw/position preparation and consent/activity hooks.
Earn API routes
app/api/starknet/earn/*
JWT-authenticated deposit/withdraw/position endpoints.
Earn UI
EarnActivityDetails/Panel.tsx, EarnConsentModal.tsx, EarnWalletForm.tsx, mobile Earn views
Renders Earn balances, activity, and consent flows.

Referral Program

Layer / File(s) Summary
Pending code storage
app/lib/pendingReferralCode.ts
Persists referral codes from share links.
Referral API routes
app/api/referral/*
Submit, claim (on-chain USDC payout), and referral-data endpoints.
Referral UI
ReferralModal.tsx, ReferralCTA.tsx, ReferralDashboardSkeleton.tsx, ReferralHubView.tsx, MainPageContent.tsx
Referral input modal, CTA, dashboard, and eligibility gating.

UI Platform

Layer / File(s) Summary
Scroll lock & login pinning
AnimatedComponents.tsx, useLoginWithScrollPin.ts
Adds body scroll lock during modals/login.
Navbar branding & network modal
Navbar.tsx, NoblocksAnimatedIcon.tsx, networkModalStore.ts, NetworkSelectionModal.tsx
New animated branding and reworked network-modal dismissal state.
Dropdown/forms
FlexibleDropdown.tsx, ExportStarknetWalletModal.tsx, CopyAddressWarningModal.tsx
Search/keyboard support and address canonicalization.
Wallet sidebar Earn/Referral integration
MobileDropdown.tsx, WalletView.tsx, WalletDetails.tsx
Wires wallet sidebar to host Earn and Referral views.

Balance Context

Layer / File(s) Summary
balancesInWei propagation
app/context/BalanceContext.tsx
Adds bigint base-unit balance tracking across all balance-building paths.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant KycModal
  participant SmileIDAPI as "/api/kyc/smile-id"
  participant SmileIDService as smileID.ts
  participant Supabase
  participant CallbackRoute as "/api/kyc/smile-id/callback"

  User->>KycModal: submit ID capture
  KycModal->>SmileIDAPI: POST images + id_info
  SmileIDAPI->>Supabase: load user_kyc_profiles, increment attempts
  SmileIDAPI->>SmileIDService: submitSmileIDJob(...)
  SmileIDService-->>SmileIDAPI: job_id, user_id
  SmileIDAPI-->>KycModal: pending/job accepted
  SmileIDService-->>CallbackRoute: async job callback
  CallbackRoute->>SmileIDService: getSmileIdJobStatus(userId, jobId)
  CallbackRoute->>Supabase: update tier, verified, platform
  KycModal->>SmileIDAPI: poll /api/kyc/status
  SmileIDAPI-->>KycModal: tier updated -> success
Loading
sequenceDiagram
  participant User
  participant EarnWalletForm
  participant DepositRoute as "/api/starknet/earn/deposit"
  participant EarnLib as earn.ts
  participant Paymaster
  participant Starknet

  User->>EarnWalletForm: enter amount, submit deposit
  EarnWalletForm->>DepositRoute: POST walletId, publicKey, amount
  DepositRoute->>EarnLib: prepareVesuDepositCalls(...)
  DepositRoute->>Paymaster: setupPaymaster, estimate fee
  DepositRoute->>Starknet: executePaymasterTransaction / deployReadyAccount
  Starknet-->>DepositRoute: transactionHash, receipt
  DepositRoute-->>EarnWalletForm: success + txHash
  EarnWalletForm->>EarnLib: refreshPosition(token)
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: chibie, sundayonah, onahprosper

Poem

A rabbit hops through tiers of trust,
Phone, then face, then bills — a must! 🐰
Vesu pools now earn their keep,
While referral carrots pile up deep. 🥕
Hop, verify, deposit, share —
This warren's build is beyond compare!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is mostly a commit log and doesn't satisfy the required template sections or testing details. Rewrite it using the template: add a clear Description, References, Testing steps/environment, and complete the checklist.
Title check ❓ Inconclusive The title is related to the PR, but it's too generic and doesn't convey the actual merge contents. Rename it to something specific, e.g. 'Merge stable into main with Starknet, KYC, referral, and Earn updates.'
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch stable

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

app/globals.css

Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): "decorators", "decorators-legacy". (1:0)


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

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 (3)
.env.example (1)

188-189: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore the Starknet env docs in .env.example
app/lib/starknet.ts still reads NEXT_PUBLIC_STARKNET_RPC_URL, STARKNET_PAYMASTER_API_KEY, and STARKNET_GAS_TOKEN_ADDRESS; removing this section leaves fresh setups without the required configuration hints.

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

In @.env.example around lines 188 - 189, Restore the Starknet environment
documentation section in .env.example so new setups still see the required
configuration hints. Re-add the entries for NEXT_PUBLIC_STARKNET_RPC_URL,
STARKNET_PAYMASTER_API_KEY, and STARKNET_GAS_TOKEN_ADDRESS in the
Starknet-related block, keeping the existing env-doc structure intact so
app/lib/starknet.ts users can configure it correctly.
app/components/WalletDetails.tsx (1)

432-443: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the Starknet wallet total in Earn mode. showEarnUi is Starknet-only, so this branch now shows crossChainTotal whenever Earn is enabled and the Starknet-specific balance branch never runs. If the header should still mirror the Starknet balance shown elsewhere, swap the conditions or render a separate Earn total.

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

In `@app/components/WalletDetails.tsx` around lines 432 - 443, The balance
rendering in WalletDetails should preserve the Starknet wallet total when Earn
mode is enabled, since the current showEarnUi branch prevents the
selectedNetwork.chain.name === "Starknet" path from ever running. Update the
conditional logic around the balance display so Starknet still shows its own
total in the header, either by checking the Starknet-specific branch before
showEarnUi or by rendering a separate Earn total explicitly in this section.
app/components/KycModal.tsx (1)

479-522: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Button copy mismatch: disclaimer references "Sign and continue" but the CTA says "Accept and continue".

✏️ Suggested fix
-            By clicking &ldquo;Sign and continue&rdquo; below, you are agreeing to the terms and policies above.
+            By clicking &ldquo;Accept and continue&rdquo; below, you are agreeing to the terms and policies above.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/KycModal.tsx` around lines 479 - 522, The CTA text in KycModal
is inconsistent with the disclaimer copy: the terms text says “Sign and
continue” while the primary action button says “Accept and continue.” Update the
wording in the KycModal component so both references use the same action phrase,
and make sure the button label and surrounding disclaimer match each other
exactly.
🟡 Minor comments (13)
app/components/PhoneVerificationModal.tsx-603-609 (1)

603-609: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace w-md with a real width utility. w-md isn’t defined here, so the class is ignored and the Cancel button keeps the base min-w-fit sizing. Use flex-1 or w-full if it should match the Continue button’s layout.

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

In `@app/components/PhoneVerificationModal.tsx` around lines 603 - 609, The Cancel
button in PhoneVerificationModal uses an undefined width class, so replace the
w-md utility on the button with a real sizing class such as flex-1 or w-full to
match the Continue button layout. Update the button markup in
PhoneVerificationModal so the existing secondaryBtnClasses still apply while the
width comes from a valid utility class.
app/lib/_starkzap-unused-shim.ts-11-23 (1)

11-23: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

new Account(...) skips the shim error. Account is backed by a proxy whose target is a plain object, so new Account(...) throws TypeError: Account is not a constructor before the get trap runs. That bypasses the intended peer-dependency message; use a constructable target plus a construct trap (and apply if it can also be called) so instantiation fails with the same error.

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

In `@app/lib/_starkzap-unused-shim.ts` around lines 11 - 23, The shim currently
only handles property access, so `new Account(...)` bypasses the intended
peer-dependency message and fails with a generic constructor error instead.
Update the proxy in `_starkzap-unused-shim.ts` so the exported `shim`/`Account`
value is constructable, and add a `construct` trap (and `apply` if the shim may
be called as a function) that throws the same descriptive error as the existing
`get` trap. Keep the error text centralized so `Account` instantiation and
access paths all surface the same optional-dependency message.
.github/workflows/ci.yml-35-36 (1)

35-36: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add persist-credentials: false to checkout.

actions/checkout@v4 persists the GITHUB_TOKEN credential to disk by default. Since this job only runs pnpm install/lint/build and doesn't need git push/write, disable persistence to reduce the blast radius if a compromised dependency (e.g. a malicious postinstall script) tries to exfiltrate it.

🔒 Proposed fix
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 35 - 36, The Checkout repository step
using actions/checkout@v4 should disable credential persistence because this CI
job only needs read access. Update the checkout configuration to set
persist-credentials to false in the workflow so the GITHUB_TOKEN is not written
to disk. Use the Checkout repository step in the workflow to locate the change.

Source: Linters/SAST tools

app/lib/config.ts-53-64 (1)

53-64: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

No non-negativity check for referral USD amounts.

Unlike kyc-tier-limits.ts's parseNonNegativeNumber (added in this same PR), these two parsers only verify Number.isFinite. A misconfigured or malformed NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD (e.g. "-5") would silently become a negative on-chain reward/qualifying-volume amount.

🛡️ Proposed fix
   referralMinQualifyingVolumeUsd: (() => {
     const parsed = parseFloat(
       process.env.NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD ?? "",
     );
-    return Number.isFinite(parsed) ? parsed : 0;
+    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
   })(),
   referralRewardAmountUsd: (() => {
     const parsed = parseFloat(
       process.env.NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD ?? "",
     );
-    return Number.isFinite(parsed) ? parsed : 0;
+    return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
   })(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/lib/config.ts` around lines 53 - 64, The referral amount parsers in
config.ts only check Number.isFinite, so negative values can slip through for
referralMinQualifyingVolumeUsd and referralRewardAmountUsd. Update these inline
parsers to use the same non-negative validation approach as
parseNonNegativeNumber from kyc-tier-limits.ts, returning 0 or rejecting invalid
input when the env var parses to a negative number, and keep the logic localized
to the two referral config fields so misconfigured NEXT_PUBLIC_REFERRAL_* values
cannot produce negative amounts.
app/components/MainPageContent.tsx-393-399 (1)

393-399: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the same wallet source for the network-modal gate

user.wallet.address can differ from the active walletAddress used by the rest of this flow, so this check should use the same source as the referral storage key. Otherwise the referral modal can bypass or miss the network-modal deferral for some wallet types.

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

In `@app/components/MainPageContent.tsx` around lines 393 - 399, The network-modal
gate in MainPageContent is checking user.wallet.address, but the rest of this
flow uses the active walletAddress source. Update this deferral check to use the
same wallet identifier as the referral storage key and related modal logic, so
the network-modal behavior stays consistent across wallet types.
app/api/kyc/tier3-verify/route.ts-116-116 (1)

116-116: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Number(...) ?? 0 does not guard against NaN.

Number() never returns null/undefined, so ?? is dead here; if tier is undefined, currentTier becomes NaN. It happens to still fail the !== 2 check (returning 403), but the intent of defaulting to 0 isn't achieved. Use || 0.

🔧 Proposed fix
-    const currentTier = Number(currentProfile.tier) ?? 0;
+    const currentTier = Number(currentProfile.tier) || 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/kyc/tier3-verify/route.ts` at line 116, The currentTier assignment in
the tier3 verification route uses Number(currentProfile.tier) ?? 0, but nullish
coalescing never catches NaN, so the intended default to 0 is not applied.
Update the currentTier calculation in the tier3-verify route to use a fallback
that handles invalid or missing tier values, such as a logical OR default, while
keeping the existing tier check logic intact.
app/api/aggregator.ts-507-599 (1)

507-599: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve HTTP status in the browser proxy path
fetchOrderDetails turns non-2xx proxy responses into a plain Error, so TransactionsContext.reconcileTransactionStatuses won’t treat 404s as ignorable AxiosErrors. Attach the status to the thrown error or rethrow an AxiosError-shaped error so downstream 404 handling still works.

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

In `@app/api/aggregator.ts` around lines 507 - 599, In fetchOrderDetails, the
browser proxy branch currently throws a plain Error on non-2xx responses, which
strips the HTTP status needed by
TransactionsContext.reconcileTransactionStatuses. Update the axios.get handling
so the thrown error preserves the response status, either by rethrowing an
AxiosError-shaped error or by attaching the status to the error object. Keep the
same behavior in the gatewayLookup and non-gateway paths, but ensure downstream
404 checks can still detect the status from the error.
app/context/TransactionsContext.tsx-211-216 (1)

211-216: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

404-ignore only covers the onramp path. fetchV2SenderPaymentOrderById throws an AxiosError, but fetchOrderDetails rethrows a plain Error for HTTP >=400, so offramp 404s still reach console.error on every poll. Preserve the status on that error or broaden the guard here.

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

In `@app/context/TransactionsContext.tsx` around lines 211 - 216, The 404
suppression in the transaction status reconciliation only catches Axios errors,
so offramp 404s coming through fetchOrderDetails still get logged on every poll.
Update either fetchOrderDetails / fetchV2SenderPaymentOrderById to preserve the
HTTP status on the thrown error, or broaden the catch in the TransactionsContext
reconciliation path to recognize the 404 from that flow. Use the existing
symbols fetchV2SenderPaymentOrderById, fetchOrderDetails, and the console.error
block in the transaction status polling logic to locate the fix.
app/api/starknet/earn/withdraw/route.ts-89-100 (1)

89-100: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Client publicKey is required but never used.

publicKey is validated as required, yet the account address and signer are derived from walletPublicKey returned by getStarknetWallet(walletId) (Lines 129–130, 152). The client-supplied publicKey is dead input here. Either drop the requirement or use it (e.g., cross-check against the server-derived key).

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

In `@app/api/starknet/earn/withdraw/route.ts` around lines 89 - 100, The withdraw
route currently treats client-supplied publicKey as required even though
Starknet account details are built from getStarknetWallet(walletId) and
walletPublicKey in route.ts. Update the handler in the withdraw route to either
remove publicKey from the required-field validation and request contract
entirely, or actually use it to verify against the server-derived
walletPublicKey before proceeding. Make the change consistently in the
validation block and any downstream logic that references the withdrawal
signer/account setup.
app/components/EarnDisclosureBanner.tsx-3-11 (1)

3-11: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Import React before using React.FC
app/components/EarnDisclosureBanner.tsx:11 uses React.FC but the module doesn’t import React, and allowUmdGlobalAccess isn’t enabled. Add import React from "react"; or switch to import type { FC } from "react" to avoid a TypeScript error.

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

In `@app/components/EarnDisclosureBanner.tsx` around lines 3 - 11, The
EarnDisclosureBanner component uses React.FC without importing React, which will
fail type-checking in this module. Update the top of EarnDisclosureBanner.tsx by
either adding a React import before the EarnDisclosureBannerProps/React.FC
usage, or changing the component typing to use a type-only FC import from react;
keep the fix aligned with the EarnDisclosureBanner export and its props
interface.
app/components/wallet-mobile-modal/WalletView.tsx-83-97 (1)

83-97: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the unreachable tab resetshowEarnUi is only true when selectedNetwork.chain.name === "Starknet", so showEarnUi && selectedNetwork.chain.name !== "Starknet" can’t fire here. Drop this effect, or flip the condition if the intent is to clear the tab when leaving Starknet.

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

In `@app/components/wallet-mobile-modal/WalletView.tsx` around lines 83 - 97, The
tab-reset effect in WalletView is unreachable because showEarnUi and
selectedNetwork.chain.name are already constrained so the condition can never be
true. Remove the useEffect entirely if it is redundant, or update the logic
around walletTab/setWalletTab to reset when the network changes away from
Starknet if that is the intended behavior.
app/components/ProfileDrawer.tsx-331-335 (1)

331-335: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace this avatar <img> with next/image
This repo extends next/core-web-vitals, so @next/next/no-img-element will flag this and can fail lint/build in CI.

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

In `@app/components/ProfileDrawer.tsx` around lines 331 - 335, The avatar in
ProfileDrawer should use Next’s optimized image component instead of a raw img
element to satisfy next/core-web-vitals linting. Update the avatar markup in
ProfileDrawer to replace the current img placeholder with next/image, keeping
the same src, alt, and sizing/rounding behavior so the existing layout is
preserved. Make sure the component imports and uses Image from next/image
wherever this avatar is rendered.
app/components/SettingsDropdown.tsx-167-167 (1)

167-167: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the embedded wallet address when clearing session keys. localStorage["userId"] is written from user.wallet.address, and both hasSeenNetworkModal-* and recipientsMigrated-* are keyed from that value. Passing the network-aware walletAddress can leave those EVM-keyed entries behind on Starknet logout; use user?.wallet?.address for cleanup or make the helper take the exact key source.

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

In `@app/components/SettingsDropdown.tsx` at line 167, The session cleanup in
SettingsDropdown’s clearUserSessionData call is using the network-aware
walletAddress fallback instead of the embedded wallet address that was used to
write the localStorage keys. Update the logout cleanup path to use
user?.wallet?.address for the session key source (or adjust clearUserSessionData
to accept the exact key origin) so the hasSeenNetworkModal-* and
recipientsMigrated-* entries are removed consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/kyc/smile-id/route.ts`:
- Around line 187-200: Normalize the SmileID biometric status checks in the KYC
route before setting verificationSuccess. In the smile-id route’s
enhanced/biometric branching, raw job_complete and job_success can be string
values, so replace the direct boolean check in the biometric path with the same
flag() normalization used in the callback flow. Keep the logic in the
smileIdResult/actions handling and ensure only a truly completed-and-successful
biometric job can promote Tier 2.

In `@app/api/kyc/tier3-verify/route.ts`:
- Around line 338-358: The outer catch in tier3-verify leaves uploaded KYC
documents orphaned and still consumes a verification attempt when
verifyUtilityBill throws. Update the route’s error handling around
verifyUtilityBill so that any thrown Dojah/network/non-2xx failure triggers
cleanup via storage.from(KYC_BUCKET).remove([path]) using the uploaded file
path, and restore the attempt counter on infrastructure failures just like the
smile-id flow. Keep the existing success and explicit-failure paths intact, but
ensure the catch block and any helper around verifyUtilityBill consistently roll
back both the uploaded object and the incremented attempt count when
verification cannot complete.

In `@app/api/kyc/transaction-summary/route.ts`:
- Around line 91-108: The onramp transaction summary logic in
transaction-summary/route.ts is treating unsupported destination currencies with
a simple amount_received fallback, which does not match the KYC limit procedure.
Update the onramp handling in the transactions mapping so it mirrors the same
branch used by the limit calculation for non-stable, non-CNGN currencies: use
the amount_sent/rate-style calculation for those rows instead of defaulting to
amount_received. Keep the existing normalization and filtering around
TRACKED_SWAP_CURRENCIES and the onrampTransactions mapping, but align the
spending fields so dailySpent/monthlySpent stay consistent with the procedure.

In `@app/api/referral/claim/route.ts`:
- Around line 385-446: The claim row handling in the referral claim flow is not
locking ownership before the transfer path, so concurrent callers can reuse the
same pending row and double-pay; update the logic around the
pendingClaimRow/pendingClaim setup to atomically transition the row into a
unique processing state with a conditional update that only succeeds from
pending or failed. If the transition returns no row, abort with a conflict/retry
response, and only continue to writeContract after the caller has exclusively
won the claim lock.

In `@app/api/referral/referral-data/route.ts`:
- Around line 279-291: The auto-claim fetch in the referral-data route is
building its base URL from request-controlled origin/host headers, which can
leak the Authorization token to an attacker. Update the self-call in
referral-data route to use a trusted server-configured base URL instead of
request.headers.get("origin") / host, and keep the Authorization forwarding only
for that safe internal target. Use the existing claimableReferrals block and the
fetch call to /api/referral/claim as the place to fix this.

In `@app/api/starknet/earn/deposit/route.ts`:
- Around line 238-286: The deposit POST in route.ts is not idempotent, so
retries can submit the same transaction twice before
`account.executePaymasterTransaction` or `deployReadyAccount` completes. Add a
persisted idempotency key or request record in the POST handler before the
transaction call, keyed off the incoming request so duplicate submissions can be
detected and short-circuited. Update the flow around `deployReadyAccount`,
`account.executePaymasterTransaction`, and the
`trackApiError`/`waitForTransaction` branches to return the prior result for
repeated requests instead of sending a second on-chain transaction.

In `@app/components/EarnWalletForm.tsx`:
- Around line 71-82: The withdraw flow is incorrectly limited to only
`availableEarnTokens`, so tokens with an existing non-zero supplied position can
disappear from the withdraw tab and selection. Update `EarnWalletForm` to union
the advertised tokens from `useEarnAvailableTokens` with any token from current
`positions` that still has `suppliedBaseUnits > 0`, and use that combined list
for the withdraw-side dropdown/tab and the `onSelect` logic. Keep the fix
localized around `tokenDropdownItems`, the withdraw tab rendering, and the
withdraw selection handlers in `EarnWalletForm`.

In `@app/components/MobileDropdown.tsx`:
- Around line 266-284: Reset the Earn-related mobile sheet state in
MobileDropdown when showEarnUi becomes false, since currentView can otherwise
remain stuck on earn, earn-deposit, or earn-withdraw and render nothing. Add the
same fallback pattern used in WalletDetails for the showEarnUi / currentView
handling, and make sure the reset also clears any selected Earn activity state
so the view returns to a valid non-Earn screen.

In `@app/lib/payment-order-id.ts`:
- Around line 40-56: The prefix parsing in payment-order-id.ts is too permissive
and can misread sender UUIDs as chain-prefixed order IDs. Update the parsing
flow in the transactionId handling logic to call isSenderPaymentOrderUuid()
before applying the /^(\d+)-(.+)$/ chain-prefix regex, and only attempt the
chainId/orderId split when the value is not a sender UUID. Keep the existing
canonicalTransactionId and returned shape in the parse routine unchanged for
valid non-UUID inputs.

---

Outside diff comments:
In @.env.example:
- Around line 188-189: Restore the Starknet environment documentation section in
.env.example so new setups still see the required configuration hints. Re-add
the entries for NEXT_PUBLIC_STARKNET_RPC_URL, STARKNET_PAYMASTER_API_KEY, and
STARKNET_GAS_TOKEN_ADDRESS in the Starknet-related block, keeping the existing
env-doc structure intact so app/lib/starknet.ts users can configure it
correctly.

In `@app/components/KycModal.tsx`:
- Around line 479-522: The CTA text in KycModal is inconsistent with the
disclaimer copy: the terms text says “Sign and continue” while the primary
action button says “Accept and continue.” Update the wording in the KycModal
component so both references use the same action phrase, and make sure the
button label and surrounding disclaimer match each other exactly.

In `@app/components/WalletDetails.tsx`:
- Around line 432-443: The balance rendering in WalletDetails should preserve
the Starknet wallet total when Earn mode is enabled, since the current
showEarnUi branch prevents the selectedNetwork.chain.name === "Starknet" path
from ever running. Update the conditional logic around the balance display so
Starknet still shows its own total in the header, either by checking the
Starknet-specific branch before showEarnUi or by rendering a separate Earn total
explicitly in this section.

---

Minor comments:
In @.github/workflows/ci.yml:
- Around line 35-36: The Checkout repository step using actions/checkout@v4
should disable credential persistence because this CI job only needs read
access. Update the checkout configuration to set persist-credentials to false in
the workflow so the GITHUB_TOKEN is not written to disk. Use the Checkout
repository step in the workflow to locate the change.

In `@app/api/aggregator.ts`:
- Around line 507-599: In fetchOrderDetails, the browser proxy branch currently
throws a plain Error on non-2xx responses, which strips the HTTP status needed
by TransactionsContext.reconcileTransactionStatuses. Update the axios.get
handling so the thrown error preserves the response status, either by rethrowing
an AxiosError-shaped error or by attaching the status to the error object. Keep
the same behavior in the gatewayLookup and non-gateway paths, but ensure
downstream 404 checks can still detect the status from the error.

In `@app/api/kyc/tier3-verify/route.ts`:
- Line 116: The currentTier assignment in the tier3 verification route uses
Number(currentProfile.tier) ?? 0, but nullish coalescing never catches NaN, so
the intended default to 0 is not applied. Update the currentTier calculation in
the tier3-verify route to use a fallback that handles invalid or missing tier
values, such as a logical OR default, while keeping the existing tier check
logic intact.

In `@app/api/starknet/earn/withdraw/route.ts`:
- Around line 89-100: The withdraw route currently treats client-supplied
publicKey as required even though Starknet account details are built from
getStarknetWallet(walletId) and walletPublicKey in route.ts. Update the handler
in the withdraw route to either remove publicKey from the required-field
validation and request contract entirely, or actually use it to verify against
the server-derived walletPublicKey before proceeding. Make the change
consistently in the validation block and any downstream logic that references
the withdrawal signer/account setup.

In `@app/components/EarnDisclosureBanner.tsx`:
- Around line 3-11: The EarnDisclosureBanner component uses React.FC without
importing React, which will fail type-checking in this module. Update the top of
EarnDisclosureBanner.tsx by either adding a React import before the
EarnDisclosureBannerProps/React.FC usage, or changing the component typing to
use a type-only FC import from react; keep the fix aligned with the
EarnDisclosureBanner export and its props interface.

In `@app/components/MainPageContent.tsx`:
- Around line 393-399: The network-modal gate in MainPageContent is checking
user.wallet.address, but the rest of this flow uses the active walletAddress
source. Update this deferral check to use the same wallet identifier as the
referral storage key and related modal logic, so the network-modal behavior
stays consistent across wallet types.

In `@app/components/PhoneVerificationModal.tsx`:
- Around line 603-609: The Cancel button in PhoneVerificationModal uses an
undefined width class, so replace the w-md utility on the button with a real
sizing class such as flex-1 or w-full to match the Continue button layout.
Update the button markup in PhoneVerificationModal so the existing
secondaryBtnClasses still apply while the width comes from a valid utility
class.

In `@app/components/ProfileDrawer.tsx`:
- Around line 331-335: The avatar in ProfileDrawer should use Next’s optimized
image component instead of a raw img element to satisfy next/core-web-vitals
linting. Update the avatar markup in ProfileDrawer to replace the current img
placeholder with next/image, keeping the same src, alt, and sizing/rounding
behavior so the existing layout is preserved. Make sure the component imports
and uses Image from next/image wherever this avatar is rendered.

In `@app/components/SettingsDropdown.tsx`:
- Line 167: The session cleanup in SettingsDropdown’s clearUserSessionData call
is using the network-aware walletAddress fallback instead of the embedded wallet
address that was used to write the localStorage keys. Update the logout cleanup
path to use user?.wallet?.address for the session key source (or adjust
clearUserSessionData to accept the exact key origin) so the
hasSeenNetworkModal-* and recipientsMigrated-* entries are removed consistently.

In `@app/components/wallet-mobile-modal/WalletView.tsx`:
- Around line 83-97: The tab-reset effect in WalletView is unreachable because
showEarnUi and selectedNetwork.chain.name are already constrained so the
condition can never be true. Remove the useEffect entirely if it is redundant,
or update the logic around walletTab/setWalletTab to reset when the network
changes away from Starknet if that is the intended behavior.

In `@app/context/TransactionsContext.tsx`:
- Around line 211-216: The 404 suppression in the transaction status
reconciliation only catches Axios errors, so offramp 404s coming through
fetchOrderDetails still get logged on every poll. Update either
fetchOrderDetails / fetchV2SenderPaymentOrderById to preserve the HTTP status on
the thrown error, or broaden the catch in the TransactionsContext reconciliation
path to recognize the 404 from that flow. Use the existing symbols
fetchV2SenderPaymentOrderById, fetchOrderDetails, and the console.error block in
the transaction status polling logic to locate the fix.

In `@app/lib/_starkzap-unused-shim.ts`:
- Around line 11-23: The shim currently only handles property access, so `new
Account(...)` bypasses the intended peer-dependency message and fails with a
generic constructor error instead. Update the proxy in
`_starkzap-unused-shim.ts` so the exported `shim`/`Account` value is
constructable, and add a `construct` trap (and `apply` if the shim may be called
as a function) that throws the same descriptive error as the existing `get`
trap. Keep the error text centralized so `Account` instantiation and access
paths all surface the same optional-dependency message.

In `@app/lib/config.ts`:
- Around line 53-64: The referral amount parsers in config.ts only check
Number.isFinite, so negative values can slip through for
referralMinQualifyingVolumeUsd and referralRewardAmountUsd. Update these inline
parsers to use the same non-negative validation approach as
parseNonNegativeNumber from kyc-tier-limits.ts, returning 0 or rejecting invalid
input when the env var parses to a negative number, and keep the logic localized
to the two referral config fields so misconfigured NEXT_PUBLIC_REFERRAL_* values
cannot produce negative amounts.

---

Nitpick comments:
In `@app/api/kyc/transaction-summary/route.ts`:
- Around line 73-89: The two Supabase reads in transactionSummaryRoute are
independent, so awaiting them one after the other in the transaction-summary
route adds avoidable latency. Update the logic around the supabaseAdmin queries
for swapTransactions and onrampTransactions to start both requests at the same
time and await them together with Promise.all, while preserving the existing
error handling for swapError and onrampError and keeping the current query
filters unchanged.

In `@app/api/referral/claim/route.ts`:
- Around line 622-627: The referral claim loop in the claim route processes
every item sequentially via tryClaimOne, which can exceed the request timeout
when many referrals are pending. Update the logic around the pendingReferrals
iteration to bound how many claims are attempted per invocation (for example,
cap the batch size or stop after a fixed number of successes/attempts), and
leave the rest for a subsequent run or background processing. Keep the change
localized to the claim handler and tryClaimOne flow so the timeout risk is
reduced without altering individual transfer handling.

In `@app/api/referral/referral-data/route.ts`:
- Around line 197-200: The fallback in toAmount is hardcoded to 1.0, which can
diverge from the configured referral reward amount used elsewhere in the claim
flow. Update the fallback logic in the referral-data route so toAmount uses the
same config.referralRewardAmountUsd value when reward_amount is null or invalid,
keeping displayed totals aligned with actual payouts.

In `@app/api/referral/submit/route.ts`:
- Around line 89-98: The referral code validation in the submit route duplicates
the shared pattern, so update the `route.ts` handler to import and use
`REFERRAL_CODE_PATTERN` from `app/lib/pendingReferralCode.ts` instead of the
inline regex. Keep the existing `normalizedCode` check and error response, but
route the validation through the shared constant so the backend and
`ReferralModal.tsx` stay aligned.

In `@app/api/v1/payment-orders/`[id]/route.ts:
- Around line 55-127: The GET handler in payment-orders route returns mixed
error envelopes, which breaks message propagation in the client. Standardize the
error responses in the `isSenderPaymentOrderUuid` config failure branch to match
the `{ status: "error", message }` shape used by the validation branches so
`fetchOrderDetails` can read the message consistently. Update the
`trackApiError` path and the `NextResponse.json` payload in the `route.ts`
handler to use one envelope format across all early returns.

In `@app/components/EarnActivityDetails.tsx`:
- Around line 20-59: The Earn components are duplicating pool config and
formatting helpers, so the copied values can drift apart. Move POOL_INFO,
formatBaseUnits, and safeBigInt into a shared client-safe helper module that
does not import the heavy SDK, and update EarnActivityDetails, EarnWalletForm,
and EarnActivityPanel to use that single source of truth. Keep the existing
lightweight/local-import rationale, but centralize the shared logic under one
set of symbols so updates to pool addresses, URLs, or formatting stay
consistent.

In `@app/components/EarnActivityPanel.tsx`:
- Around line 15-62: The Earn formatting and APY helpers are duplicated between
EarnActivityPanel and EarnHubView, so consolidate them into one shared helper
module and import from both places. Move TOKEN_DECIMALS, TOKEN_FACTOR,
APR_REFRESH_MS, formatBaseUnits, formatPercent, safeBigInt, and projectEarnings
into a shared utility, then update both components to use the shared symbols so
formatting and earnings math stay consistent in one place.

In `@app/components/EarnConsentModal.tsx`:
- Around line 69-77: The DialogPanel in EarnConsentModal currently uses an
inline window.innerWidth check for maxWidth, which duplicates the existing
responsive behavior and won’t update on resize. Move this sizing logic into
Tailwind responsive classes on the DialogPanel so it matches the rest of the
component’s sm: breakpoint usage, and remove the inline style-based width
condition entirely.

In `@app/components/EarnWalletForm.tsx`:
- Around line 42-56: The earn amount formatting logic is duplicated between
EarnWalletForm and EarnActivityDetails, so extract the shared helpers into a
common earn formatting utility module. Move the equivalent of formatBaseUnits
and the BigInt parsing/safety helper used by parseAmountToBaseUnits/safeBigInt
into a reusable module, then update EarnWalletForm and EarnActivityDetails to
import and use those shared functions to keep formatting behavior consistent.

In `@app/components/KycModal.tsx`:
- Around line 1092-1138: The Tier3 upload flow duplicates the same extension and
size validation in both handleTier3FileChange and handleTier3Drop inside
renderTier3Upload. Extract that shared JPG/JPEG/PNG and 5 MB validation into one
reusable helper near ALLOWED_TIER3_EXTENSIONS and TIER3_MAX_BYTES, then have
both handlers call it and only keep their input/drop-specific file assignment
and reset behavior.

In `@app/components/KycPolicy.tsx`:
- Around line 254-258: The `KycPolicy` component is using `next/link` for a
`mailto:` address, which should be a plain anchor instead. Update the email link
in `KycPolicy` (and the other matching `mailto:` occurrences) to use an `<a>`
tag with the same href and styling, leaving `next/link` only for internal
navigation.
- Around line 1-379: The KycPolicy component duplicates nearly the same legal
boilerplate already present in PrivacyPolicy, so future edits can drift out of
sync. Refactor the shared policy text into a single reusable source (for
example, section data rendered by both KycPolicy and PrivacyPolicy) and keep
only document-specific formatting differences in each component. Use the
KycPolicy and PrivacyPolicy components as the main reference points when
consolidating the repeated sections.

In `@app/components/MobileDropdown.tsx`:
- Around line 242-264: There are two overlapping reset effects in MobileDropdown
that both set currentView to "wallet" when isOpen becomes false; keep only the
broader reset effect and remove the earlier one. Consolidate the close/reset
behavior in the single useEffect that already handles selectedEarnActivity,
earnActivityReturnView, selectedTransaction, and isNetworkListOpen so the wallet
reset happens in one place and future changes stay consistent.

In `@app/components/Navbar.tsx`:
- Around line 173-192: The conditional in Navbar’s logo rendering is now
redundant because both the `IS_MAIN_PRODUCTION_DOMAIN` branches render the same
`NobblocksWorldCupLogo` and `NobblocksAnimatedIcon`; either collapse the
branches into a single shared block or restore distinct production/beta logos if
the flag is still needed. Also update the `classNames` usage for the arrow in
`Navbar` so the `IS_MAIN_PRODUCTION_DOMAIN` offset logic and its comment match
the actual rendered logo state, since the current “beta logo” adjustment is
stale.

In `@app/components/NetworkSelectionModal.tsx`:
- Line 30: The commented-out legacy code in NetworkSelectionModal is dead code
and should be removed now that the networkModalStore-based flow is used. Delete
the leftover NetworkSelectionModal signature comment and the old handleClose
implementation, and make sure the active component logic stays centered around
the NetworkSelectionModal component and its current store-driven handlers.

In `@app/components/NoblocksAnimatedIcon.tsx`:
- Around line 34-37: The stripSize helper currently only matches SVGs that start
exactly with <svg, so it can miss assets with an XML prolog, BOM, or leading
comments. Update stripSize in NoblocksAnimatedIcon.tsx to locate and modify the
first svg opening tag anywhere near the start of the string, and keep the
width/height removal logic in that helper so re-exported assets still have their
size stripped consistently.
- Around line 108-126: The `NoblocksAnimatedIcon` component injects fetched SVG
strings via `dangerouslySetInnerHTML` for `ball` and `trophy`, so sanitize those
markup strings before rendering to close off XSS risk. Update the
SVG-fetching/assignment path used by `NoblocksAnimatedIcon` to pass the returned
markup through `DOMPurify.sanitize()` (or equivalent) before it reaches the
`<span dangerouslySetInnerHTML>` blocks, keeping the current phase/animation
behavior unchanged.

In `@app/components/NoblocksWorldCupLogo.tsx`:
- Around line 49-55: The NoblocksWorldCupLogo component is using the same unsafe
innerHTML injection pattern as NoblocksAnimatedIcon, so sanitize the fetched SVG
markup before rendering it. Update the logic around the svg payload and the span
that uses dangerouslySetInnerHTML so the markup is passed through
DOMPurify.sanitize() first, keeping the existing same-origin/static-asset
behavior while removing the static-analysis warning.
- Line 18: The current cache in NoblocksWorldCupLogo only stores the resolved
markup, so concurrent mounts can still trigger duplicate fetches before it is
populated. Update the caching logic around cachedMarkup and the fetch/parse path
in the component so an in-flight promise is shared and reused by all callers
until it resolves, then replace it with the final markup string. Make sure the
logic in the component’s fetch helper and render path dedupes simultaneous
requests without changing the existing cachedMarkup behavior after resolution.

In `@app/components/PhoneVerificationModal.tsx`:
- Around line 544-576: The OTP inputs in PhoneVerificationModal currently only
handle one digit per box and do not support SMS autofill or pasting a full code.
Update the mapped input fields in the OTP entry section to enable one-time-code
autofill and numeric mobile input, and add paste handling so a full 6-digit code
can be distributed across the inputs instead of only filling the first one.

In `@app/components/ReferralModal.tsx`:
- Around line 123-138: The referral code field in ReferralModal is missing an
accessible label association because the label and input are siblings without a
link. Update the label/input pair in ReferralModal so the label uses htmlFor and
the input has a matching id, keeping the existing “Enter referral code” text and
the referralCode state behavior unchanged.

In `@app/components/wallet-mobile-modal/ReferralHubView.tsx`:
- Around line 27-83: Replace the `any` usage in `ReferralHubView` with the
shared referral response types from the `referral-data` flow so `referralData`
and `filteredReferrals` are strongly typed. Update the `useState` for
`referralData` and the `filter` callback to use the `ReferralData` shape (and
the referral item type) instead of `any`, so accesses like `referral_code`,
`referrals`, `status`, and other fields are compile-time checked.

In `@app/components/wallet-mobile-modal/WalletView.tsx`:
- Around line 105-197: The sticky tab bar and network dropdown markup in
WalletView is duplicated from the similar section in WalletDetails, so
consolidate this repeated UI into a shared component such as WalletTabBar or
NetworkTabSelector. Move the tab buttons, network picker, and
AnimatePresence/motion dropdown behavior into that reusable component, then have
WalletView and WalletDetails pass in the selected tab, network state, and
callbacks so there is a single source of truth for this interaction.

In `@app/context/BalanceContext.tsx`:
- Around line 117-121: The repeated buildWalletBalancesFromRaw(rawBalances,
rate, balancesInWei) call pattern in BalanceContext should be consolidated to
reduce duplication and avoid missing future fields. Update
buildWalletBalancesFromRaw to accept a single object or fetch-result shape that
carries both balances and balancesInWei, then refactor each call site to pass
that unified value instead of three positional arguments. Use the existing
buildWalletBalancesFromRaw helper and the surrounding balance-fetch flow in
BalanceContext to keep the change consistent across all call sites.

In `@app/context/StarknetContext.tsx`:
- Around line 104-106: The empty-string ternary before calling
toCanonicalStarknetAddress in StarknetContext is redundant because the helper
already normalizes nullish and empty input. Simplify the address assignment by
passing storedAddress directly into toCanonicalStarknetAddress and let that
function handle empty values, keeping the change localized to the address
normalization logic in StarknetContext.

In `@app/hooks/useEarnHandler.ts`:
- Around line 204-334: The Earn API calls in refreshPosition, deposit, and
withdraw can hang indefinitely because they use fetch without a timeout. Add an
AbortController-based timeout around the fetches in useEarnHandler.ts so these
requests fail fast and reject instead of leaving the UI waiting forever. Update
the refreshPosition, deposit, and withdraw callbacks together, and ensure any
timeout cleanup is handled consistently in these helper methods.

In `@app/lib/earn.ts`:
- Around line 187-202: `getSupplyApy` currently calls
`provider.getMarkets(ChainId.MAINNET)` on every lookup, which makes the hot
`getVesuPosition` path refetch the full market list repeatedly. Add a
short-lived in-memory cache for the markets result (or for the `getSupplyApy`
lookup) with a small TTL, and reuse it before calling `provider.getMarkets`
again. Keep the change localized around `getSupplyApy` and the `getMarkets`
fetch so repeated `/api/starknet/earn/position` requests avoid redundant
external calls.

In `@app/lib/networkModalStore.ts`:
- Around line 29-44: The case-insensitive localStorage key scanning is
duplicated between hasSeenNetworkModalFlag and clearNetworkModalSeen, so extract
the shared matching logic into a helper such as findMatchingKeys(target: string)
and reuse it from both places. Keep the target computation based on
NETWORK_MODAL_STORAGE_KEY_PREFIX and walletAddress in one place, and centralize
the localStorage iteration plus try/catch handling inside the helper. Update
both hasSeenNetworkModalFlag and clearNetworkModalSeen to call the shared helper
instead of reimplementing the scan.

In `@app/lib/phone-verification.ts`:
- Around line 122-136: The KudiSMS response handling in phone-verification
should check response.ok or response.status before calling response.json(),
since a non-2xx response may return a non-JSON body and cause an exception.
Update the logic around the response parsing in the KudiSMS send flow to handle
HTTP failures explicitly first, then only parse JSON when safe, and keep the
existing success/failure return shape in the same code path.

In `@app/lib/smileCameraTheme.ts`:
- Around line 288-306: The continuous requestAnimationFrame loop in
startSmileCameraAlertStyleFix is re-running applySmileCameraFixes(host) every
frame, which creates an unnecessary hot path. Change the tick scheduling so the
Smile camera style fix only reapplies on a throttled interval or in response to
DOM changes via a MutationObserver, and keep the cleanup in the returned
teardown function by canceling the scheduled work and calling
uninstallOverrideSheets(host).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3012d16c-9859-4f5f-9081-65ac9967338a

📥 Commits

Reviewing files that changed from the base of the PR and between 8746342 and e05199d.

⛔ Files ignored due to path filters (16)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/icons/placeholder.png is excluded by !**/*.png
  • public/images/avatar/Avatar.png is excluded by !**/*.png
  • public/images/avatar/Avatar1.png is excluded by !**/*.png
  • public/images/avatar/Avatar2.png is excluded by !**/*.png
  • public/images/avatar/Avatar3.png is excluded by !**/*.png
  • public/images/avatar/Avatar4.png is excluded by !**/*.png
  • public/images/avatar/Avatar5.png is excluded by !**/*.png
  • public/images/avatar/Avatar6.png is excluded by !**/*.png
  • public/images/avatar/Avatar7.png is excluded by !**/*.png
  • public/images/referral-cta-dollar.png is excluded by !**/*.png
  • public/images/referral-cta.png is excluded by !**/*.png
  • public/images/referral-graphic.png is excluded by !**/*.png
  • public/logos/noblocks-worldcup-logo.svg is excluded by !**/*.svg
  • public/logos/worldcup/ball.svg is excluded by !**/*.svg
  • public/logos/worldcup/trophy.svg is excluded by !**/*.svg
📒 Files selected for processing (140)
  • .env.example
  • .github/CODEOWNERS
  • .github/workflows/ci.yml
  • app/api/aggregator.ts
  • app/api/blockfest/cashback/route.ts
  • app/api/internal/set-wallet-context/route.ts
  • app/api/kyc/smile-id/callback/route.ts
  • app/api/kyc/smile-id/id_types.json
  • app/api/kyc/smile-id/route.ts
  • app/api/kyc/status/route.ts
  • app/api/kyc/tier3-verify/route.ts
  • app/api/kyc/transaction-summary/route.ts
  • app/api/phone/send-otp/route.ts
  • app/api/phone/verify-otp/route.ts
  • app/api/referral/claim/route.ts
  • app/api/referral/referral-data/route.ts
  • app/api/referral/submit/route.ts
  • app/api/starknet/create-order/route.ts
  • app/api/starknet/earn/deposit/route.ts
  • app/api/starknet/earn/position/route.ts
  • app/api/starknet/earn/withdraw/route.ts
  • app/api/starknet/export-wallet/route.ts
  • app/api/starknet/transfer/route.ts
  • app/api/v1/payment-orders/[id]/route.ts
  • app/api/v1/transactions/[id]/route.ts
  • app/api/v1/transactions/route.ts
  • app/api/v1/transactions/swap-precheck/route.ts
  • app/api/v1/wallets/deprecate/route.ts
  • app/components/AnimatedComponents.tsx
  • app/components/AppLayout.tsx
  • app/components/CopyAddressWarningModal.tsx
  • app/components/EarnActivityDetails.tsx
  • app/components/EarnActivityPanel.tsx
  • app/components/EarnConsentModal.tsx
  • app/components/EarnDisclosureBanner.tsx
  • app/components/EarnWalletForm.tsx
  • app/components/ExportStarknetWalletModal.tsx
  • app/components/FlexibleDropdown.tsx
  • app/components/FundWalletForm.tsx
  • app/components/KycModal.tsx
  • app/components/KycPolicy.tsx
  • app/components/MainPageContent.tsx
  • app/components/MobileDropdown.tsx
  • app/components/Navbar.tsx
  • app/components/NetworkSelectionModal.tsx
  • app/components/NoblocksAnimatedIcon.tsx
  • app/components/NoblocksWorldCupLogo.tsx
  • app/components/PhoneVerificationModal.tsx
  • app/components/PrivacyPolicy.tsx
  • app/components/ProfileDrawer.tsx
  • app/components/ReferralCTA.tsx
  • app/components/ReferralDashboardSkeleton.tsx
  • app/components/ReferralModal.tsx
  • app/components/SettingsDropdown.tsx
  • app/components/TransactionLimitModal.tsx
  • app/components/TransferForm.tsx
  • app/components/WalletDetails.tsx
  • app/components/index.ts
  • app/components/kyc/DocumentRequirementsModal.tsx
  • app/components/kyc/index.ts
  • app/components/recipient/RecipientDetailsForm.tsx
  • app/components/wallet-mobile-modal/EarnActivityDetailView.tsx
  • app/components/wallet-mobile-modal/EarnHubView.tsx
  • app/components/wallet-mobile-modal/ReferralHubView.tsx
  • app/components/wallet-mobile-modal/SettingsView.tsx
  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/components/wallet-mobile-modal/index.ts
  • app/context/BalanceContext.tsx
  • app/context/KYCContext.tsx
  • app/context/MigrationContext.tsx
  • app/context/StarknetContext.tsx
  • app/context/TransactionsContext.tsx
  • app/context/index.ts
  • app/globals.css
  • app/hooks/useEarnAccess.ts
  • app/hooks/useEarnHandler.ts
  • app/hooks/useLoginWithScrollPin.ts
  • app/hooks/useSwapButton.ts
  • app/kyc-policy/kyc-client.tsx
  • app/kyc-policy/page.tsx
  • app/lib/_starkzap-unused-shim.ts
  • app/lib/config.ts
  • app/lib/countries.ts
  • app/lib/dojah.ts
  • app/lib/earn.ts
  • app/lib/earnConsent.ts
  • app/lib/earnFeature.ts
  • app/lib/kyc-tier-limits.ts
  • app/lib/kyc-upgrade-path.ts
  • app/lib/networkModalStore.ts
  • app/lib/payment-order-id.ts
  • app/lib/pendingReferralCode.ts
  • app/lib/phone-validation.ts
  • app/lib/phone-verification.ts
  • app/lib/privy.ts
  • app/lib/smileCameraTheme.ts
  • app/lib/smileID.ts
  • app/lib/smileIdIdValidation.ts
  • app/lib/starknet.ts
  • app/lib/supabase.ts
  • app/lib/swap-transaction-limit-server.ts
  • app/lib/transaction-wallet-auth.ts
  • app/lib/validation.ts
  • app/mainContent.tsx
  • app/pages/TransactionForm.tsx
  • app/pages/TransactionPreview.tsx
  • app/pages/TransactionStatus.tsx
  • app/privacy-policy/privacy-client.tsx
  • app/providers.tsx
  • app/sitemap.ts
  • app/terms/terms-client.tsx
  • app/types.ts
  • app/types/smart-camera-web.d.ts
  • app/utils.ts
  • docs/environment-variables.md
  • docs/transaction-history-setup.md
  • jest.setup.js
  • middleware.ts
  • next.config.mjs
  • package.json
  • patches/@privy-io__react-auth@3.1.0.patch
  • scripts/package.json
  • scripts/test-starkzap.ts
  • supabase/.gitignore
  • supabase/config.toml
  • supabase/migrations/20260508140001_create_transactions_table.sql
  • supabase/migrations/20260508140002_create_user_kyc_profiles.sql
  • supabase/migrations/20260508140003_create_wallet_migrations.sql
  • supabase/migrations/20260508140004_create_saved_recipients.sql
  • supabase/migrations/20260508140005_create_refund_accounts.sql
  • supabase/migrations/20260508140006_create_blockfest_participants.sql
  • supabase/migrations/20260508140007_create_blockfest_cashback_claims.sql
  • supabase/migrations/20260508140010_create_kyc_documents_storage_bucket.sql
  • supabase/migrations/20260611120000_kyc_identity_hardening.sql
  • supabase/migrations/20260612130000_fix_kyc_limit_status_filters.sql
  • supabase/migrations/20260616000000_add_referrer_rewards_unlocked_at.sql
  • supabase/migrations/20260617120000_tier3_unlimited_kyc_limit.sql
  • supabase/migrations/add_referrals_referred_wallet_unique.sql
  • supabase/migrations/create_transactions_table.sql
  • supabase/seed.sql
💤 Files with no reviewable changes (1)
  • app/components/AppLayout.tsx

Comment on lines +187 to +200
const actions = smileIdResult?.Actions;
const isEnhancedKyc = actions?.Verify_ID_Number !== undefined;
const isBiometricKyc = !isEnhancedKyc;

let verificationSuccess = false;

if (isEnhancedKyc) {
// Enhanced KYC: Check if ID verification passed
verificationSuccess = actions.Verify_ID_Number === "Verified";
} else if (isBiometricKyc) {
// Biometric KYC: job is complete — check whether it succeeded
verificationSuccess =
smileIdResult.job_complete && smileIdResult.job_success;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files | rg '(^|/)(app/api/kyc/smile-id/route\.ts|.*smile.*id.*\.(ts|tsx|js|jsx)|.*Smile.*ID.*\.(ts|tsx|js|jsx)|.*smile.*\.md)$' || true

printf '\n== Outline route.ts ==\n'
ast-grep outline app/api/kyc/smile-id/route.ts --view expanded || true

printf '\n== Relevant slices route.ts ==\n'
nl -ba app/api/kyc/smile-id/route.ts | sed -n '120,240p'

printf '\n== Search for flag helper and SmileID response access ==\n'
rg -n 'flag\(|job_complete|job_success|Actions|ResultText|result\.Actions|result\.ResultText|jobStatus\.result' app api lib src . -g '!node_modules' || true

Repository: paycrest/noblocks

Length of output: 624


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== route.ts (1-260) ==\n'
cat -n app/api/kyc/smile-id/route.ts | sed -n '1,260p'

printf '\n== callback/route.ts (1-260) ==\n'
cat -n app/api/kyc/smile-id/callback/route.ts | sed -n '1,260p'

printf '\n== smileIdIdValidation.ts ==\n'
cat -n app/lib/smileIdIdValidation.ts | sed -n '1,260p'

Repository: paycrest/noblocks

Length of output: 28379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app/lib/smileID.ts (1-320) ==\n'
cat -n app/lib/smileID.ts | sed -n '1,320p'

printf '\n== Search for job_complete/job_success typing ==\n'
rg -n 'job_complete|job_success|Actions|ResultText|result\?' app/lib/smileID.ts app/api/kyc/smile-id -g '!node_modules' || true

Repository: paycrest/noblocks

Length of output: 9548


🌐 Web query:

SmileID submit_job return_job_status response shape Actions ResultText job_complete job_success result.Actions

💡 Result:

When using the Smile ID API, the response returned by the job status check provides a comprehensive JSON object detailing the outcome of the requested job [1][2][3]. The top-level response structure includes fields such as job_complete (a boolean indicating if a final status has been reached) and job_success (a boolean indicating if the match or authentication was successful) [1][2][3]. The result object contains the core outcome data [1][2][3]: - ResultText: A human-readable textual value of the job outcome [1][4][5]. - SmileJobID: The internal Smile ID reference number for the job [1][4]. - PartnerParams: An object containing the original user_id, job_id, and job_type passed in the request [1][2][3]. - ConfidenceValue: A numerical representation of the confidence in the final determination [1][2]. Additionally, the Actions object provides a granular breakdown of the specific checks performed by Smile ID [4][5]. This object contains key-value pairs where the keys represent individual checks (e.g., Liveness_Check, Selfie_Check, Selfie_To_Registered_Selfie_Compare) and the values indicate the status of those checks (e.g., Passed, Not Applicable, Completed) [4][5][6]. Depending on your request configuration, the response may also include an image_links object containing signed URLs for captured media, provided the appropriate flags were set during the submission or status query [1][2][7][8].

Citations:


🌐 Web query:

smile-identity-core submit_job return_job_status response result.Actions result.ResultText top-level Actions

💡 Result:

In the Smile Identity API, the job response structure provides details about the outcome of a job. When using the job status endpoint (often accessed via methods like getJobStatus in the Core SDKs), the response contains several key fields that describe the final result [1][2][3]. Key response fields include: ResultText: A human-readable string indicating the outcome of the job (e.g., "ID Number Validated", "Document Verified") [1][4][2][5]. Actions: An object containing the specific results of individual checks performed by Smile Identity during the job [4][2][5]. For example, in an Enhanced KYC job, this object might include keys like Verify_ID_Number or Return_Personal_Info, with values such as "Verified" or "Returned" [2][5]. In document verification, it may include fields like Human_Review_Compare or Verify_Document [4]. While your query mentioned result.Actions and result.ResultText, note that depending on the specific product (e.g., Enhanced KYC vs. standard Job Status), these fields may appear either at the top level of the response object or nested within a result object [1][2][5]. You should always refer to the specific product documentation for the exact JSON schema, as structural nuances can vary based on the request type [2][5]. To retrieve this data in your application, you typically configure options such as return_job_status=true when calling the submission or status methods [6][7][3]. This ensures the API returns the full job result rather than just an acknowledgment of submission [1][3].

Citations:


Normalize SmileID status flags before promoting Tier 2. job_complete and job_success can arrive as "true"/"false" strings, so the raw && here can treat a failed biometric job as success and promote the user. Reuse the same flag() normalization used in the callback route.

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

In `@app/api/kyc/smile-id/route.ts` around lines 187 - 200, Normalize the SmileID
biometric status checks in the KYC route before setting verificationSuccess. In
the smile-id route’s enhanced/biometric branching, raw job_complete and
job_success can be string values, so replace the direct boolean check in the
biometric path with the same flag() normalization used in the callback flow.
Keep the logic in the smileIdResult/actions handling and ensure only a truly
completed-and-successful biometric job can promote Tier 2.

Comment on lines +338 to +358
} catch (err) {
const raw = err instanceof Error ? err.message : String(err);
console.error("[tier3-verify] unexpected error", err);
// Dojah often returns JSON in the thrown message; avoid double-encoding for clients.
let message = raw;
if (raw.trim().startsWith("{")) {
try {
const parsed = JSON.parse(raw) as { error?: string; message?: string };
message =
(typeof parsed.error === "string" && parsed.error) ||
(typeof parsed.message === "string" && parsed.message) ||
raw;
} catch {
// keep raw
}
}
return NextResponse.json(
{ success: false, error: message || "Verification failed" },
{ status: 500 },
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Uploaded document is orphaned when verifyUtilityBill throws.

The file is uploaded at Line 202 and the signed URL created before verifyUtilityBill runs (Line 248). On a thrown Dojah error (timeout, network, non-2xx) control jumps to this outer catch, which returns 500 but never calls storage.from(KYC_BUCKET).remove([path]) — unlike the explicit-failure paths (Lines 231, 258, 311, 322). Each such failure leaves an unreferenced object in the private KYC bucket.

Also note the attempt counter incremented at Line 160 is not restored on these infrastructure failures, so a Dojah timeout still burns one of the user's 5 attempts (the smile-id route deliberately restores the counter for infrastructure outages).

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

In `@app/api/kyc/tier3-verify/route.ts` around lines 338 - 358, The outer catch in
tier3-verify leaves uploaded KYC documents orphaned and still consumes a
verification attempt when verifyUtilityBill throws. Update the route’s error
handling around verifyUtilityBill so that any thrown Dojah/network/non-2xx
failure triggers cleanup via storage.from(KYC_BUCKET).remove([path]) using the
uploaded file path, and restore the attempt counter on infrastructure failures
just like the smile-id flow. Keep the existing success and explicit-failure
paths intact, but ensure the catch block and any helper around verifyUtilityBill
consistently roll back both the uploaded object and the incremented attempt
count when verification cannot complete.

Comment on lines +91 to +108
const error = swapError ?? onrampError;
const transactions = [
// The tracked-currency filter lives here rather than in the query because
// PostgREST cannot compare a column case-insensitively in .in().
...(swapTransactions ?? [])
.map((tx) => ({
...tx,
kind: "offramp" as const,
from_currency: normalizeCurrency(tx.from_currency),
}))
.filter((tx) => TRACKED_SWAP_CURRENCIES.has(tx.from_currency)),
...(onrampTransactions ?? []).map((tx) => ({
...tx,
kind: "onramp" as const,
from_currency: normalizeCurrency(tx.from_currency),
to_currency: normalizeCurrency(tx.to_currency),
})),
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for the full set of supported onramp to_currency values
rg -n "to_currency" --type=ts -C2 app
rg -n "TRACKED_SWAP_CURRENCIES|STABLE_TO_CURRENCIES" --type=ts

Repository: paycrest/noblocks

Length of output: 11782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== route outline =="
ast-grep outline app/api/kyc/transaction-summary/route.ts --view expanded || true

echo
echo "== route slice around transaction assembly and usdAmount logic =="
sed -n '1,230p' app/api/kyc/transaction-summary/route.ts

echo
echo "== currency set definitions/usages =="
rg -n "TRACKED_SWAP_CURRENCIES|STABLE_TO_CURRENCIES|USD_STABLE|normalizeCurrency" app -C 2

Repository: paycrest/noblocks

Length of output: 11608


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== transaction creation route snippets =="
sed -n '1,420p' app/api/v1/transactions/route.ts

echo
echo "== searches for onramp currency constraints/validation =="
rg -n "onramp|toCurrency|to_currency|fromCurrency|from_currency|USDC|USDT|CUSD|CNGN" app lib supabase db prisma -g '!**/node_modules/**' -C 2

echo
echo "== SQL/schema files mentioning transactions or currency constraints =="
fd -a -t f '.*(sql|tsx?|ts)$' . | rg 'migrations|supabase|schema|transaction|currency|database' -n

Repository: paycrest/noblocks

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== usages of transaction-summary endpoint =="
rg -n "/api/kyc/transaction-summary|transaction-summary" app supabase -g '!**/node_modules/**' -C 3

echo
echo "== nearby KYC summary / limit-related code =="
rg -n "dailySpent|monthlySpent|hasCngnTxs|partial" app supabase -g '!**/node_modules/**' -C 2

Repository: paycrest/noblocks

Length of output: 14191


Align unsupported onramp currencies with the KYC limit logic

app/api/kyc/transaction-summary/route.ts falls back to amount_received for onramp rows whose to_currency is neither stable nor CNGN, but the KYC limit procedure counts those rows via amount_sent / rate. That can overstate dailySpent/monthlySpent for onramp currencies outside STABLE_TO_CURRENCIES ∪ {CNGN} without hitting hasCngnTxs. Mirror the procedure’s branch here instead of the 1:1 fallback.

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

In `@app/api/kyc/transaction-summary/route.ts` around lines 91 - 108, The onramp
transaction summary logic in transaction-summary/route.ts is treating
unsupported destination currencies with a simple amount_received fallback, which
does not match the KYC limit procedure. Update the onramp handling in the
transactions mapping so it mirrors the same branch used by the limit calculation
for non-stable, non-CNGN currencies: use the amount_sent/rate-style calculation
for those rows instead of defaulting to amount_received. Keep the existing
normalization and filtering around TRACKED_SWAP_CURRENCIES and the
onrampTransactions mapping, but align the spending fields so
dailySpent/monthlySpent stay consistent with the procedure.

Comment on lines +385 to +446
let pendingClaimRow: typeof existingClaim;

if (existingClaim?.status === "pending") {
pendingClaimRow = existingClaim;
} else if (existingClaim?.status === "failed") {
const { data: retried, error: retryErr } = await supabaseAdmin
.from("referral_claims")
.update({ status: "pending", tx_hash: null, updated_at: new Date().toISOString() })
.eq("id", existingClaim.id)
.select()
.single();
if (retryErr || !retried) {
return { success: false, code: "CLAIM_RETRY_FAILED", message: "Unable to retry this claim." };
}
pendingClaimRow = retried;
} else {
const { data: inserted, error: insertError } = await supabaseAdmin
.from("referral_claims")
.insert({
referral_id: referralId,
wallet_address: walletAddress,
reward_amount: referralRewardAmountUsd,
status: "pending",
})
.select()
.single();

if (insertError && isUniqueViolation(insertError)) {
const { data: raced, error: raceErr } = await supabaseAdmin
.from("referral_claims")
.select("*")
.eq("referral_id", referralId)
.eq("wallet_address", walletAddress)
.single();
if (raceErr || !raced) {
return { success: false, code: "CLAIM_CONFLICT", message: "Unable to resolve claim row." };
}
if (raced.status === "completed") {
return { success: true, txHash: raced.tx_hash, amount: raced.reward_amount };
}
if (raced.status === "failed") {
const { data: reopened, error: reopenErr } = await supabaseAdmin
.from("referral_claims")
.update({ status: "pending", tx_hash: null, updated_at: new Date().toISOString() })
.eq("id", raced.id)
.select()
.single();
if (reopenErr || !reopened) {
return { success: false, code: "CLAIM_RETRY_FAILED", message: "Unable to retry this claim." };
}
pendingClaimRow = reopened;
} else {
pendingClaimRow = raced;
}
} else if (insertError || !inserted) {
return { success: false, code: "CLAIM_CREATION_FAILED", message: "Unable to create claim record." };
} else {
pendingClaimRow = inserted;
}
}

const pendingClaim = pendingClaimRow!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file first, then inspect the relevant sections.
ast-grep outline app/api/referral/claim/route.ts --view expanded

echo "---- lines around tryClaimOne / claim creation ----"
sed -n '340,480p' app/api/referral/claim/route.ts

echo "---- lines around GET / POST handlers ----"
sed -n '480,700p' app/api/referral/claim/route.ts

echo "---- search for claim status transitions and writeContract usage ----"
rg -n "writeContract|status:\s*\"processing\"|status:\s*\"pending\"|status:\s*\"failed\"|status:\s*\"completed\"|referral_claims" app/api/referral/claim/route.ts app -g '!**/node_modules/**'

Repository: paycrest/noblocks

Length of output: 18695


Add an atomic claim lock before writeContract
pending rows are reused without any exclusive state transition, so two concurrent callers can both reach the transfer path and pay the same referral twice (for example, auto-claim GET and manual POST). Move the row into a unique processing state with an UPDATE ... WHERE status IN ('pending','failed') RETURNING *-style claim, and continue only if one caller wins.

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

In `@app/api/referral/claim/route.ts` around lines 385 - 446, The claim row
handling in the referral claim flow is not locking ownership before the transfer
path, so concurrent callers can reuse the same pending row and double-pay;
update the logic around the pendingClaimRow/pendingClaim setup to atomically
transition the row into a unique processing state with a conditional update that
only succeeds from pending or failed. If the transition returns no row, abort
with a conflict/retry response, and only continue to writeContract after the
caller has exclusively won the claim lock.

Comment on lines +279 to +291
if (claimableReferrals.length > 0) {
const authHeader = request.headers.get("Authorization");
const origin = request.headers.get("origin") || `https://${request.headers.get("host")}`;
fetch(`${origin}/api/referral/claim`, {
method: "GET",
headers: {
...(authHeader ? { Authorization: authHeader } : {}),
"x-user-id": userId!,
},
}).catch((e) =>
console.error("Auto-claim background request failed:", e),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Host-header injection can exfiltrate the bearer token.

origin is derived from client-controlled origin/host headers, then the Authorization header is forwarded to ${origin}/api/referral/claim. An attacker who sets a spoofed Host (or Origin) header causes this server to send the user's bearer token to an attacker-controlled host. Build the self-call base URL from a trusted, server-configured value (e.g., an env var / NEXT_PUBLIC_APP_URL) rather than request headers.

🔒 Suggested change
-            const authHeader = request.headers.get("Authorization");
-            const origin = request.headers.get("origin") || `https://${request.headers.get("host")}`;
+            const authHeader = request.headers.get("Authorization");
+            const origin = process.env.NEXT_PUBLIC_APP_URL; // trusted, server-configured base URL
+            if (origin) {
             fetch(`${origin}/api/referral/claim`, {
               method: "GET",
               headers: {
                 ...(authHeader ? { Authorization: authHeader } : {}),
                 "x-user-id": userId!,
               },
             }).catch((e) =>
               console.error("Auto-claim background request failed:", e),
             );
+            }
📝 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
if (claimableReferrals.length > 0) {
const authHeader = request.headers.get("Authorization");
const origin = request.headers.get("origin") || `https://${request.headers.get("host")}`;
fetch(`${origin}/api/referral/claim`, {
method: "GET",
headers: {
...(authHeader ? { Authorization: authHeader } : {}),
"x-user-id": userId!,
},
}).catch((e) =>
console.error("Auto-claim background request failed:", e),
);
}
if (claimableReferrals.length > 0) {
const authHeader = request.headers.get("Authorization");
const origin = process.env.NEXT_PUBLIC_APP_URL; // trusted, server-configured base URL
if (origin) {
fetch(`${origin}/api/referral/claim`, {
method: "GET",
headers: {
...(authHeader ? { Authorization: authHeader } : {}),
"x-user-id": userId!,
},
}).catch((e) =>
console.error("Auto-claim background request failed:", e),
);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/referral/referral-data/route.ts` around lines 279 - 291, The
auto-claim fetch in the referral-data route is building its base URL from
request-controlled origin/host headers, which can leak the Authorization token
to an attacker. Update the self-call in referral-data route to use a trusted
server-configured base URL instead of request.headers.get("origin") / host, and
keep the Authorization forwarding only for that safe internal target. Use the
existing claimableReferrals block and the fetch call to /api/referral/claim as
the place to fix this.

Comment on lines +238 to +286
let result;
try {
if (!isDeployed) {
result = await deployReadyAccount({
walletId,
publicKey: walletPublicKey,
classHash,
userJwt: token,
userId: authUserId,
origin,
calls,
});
} else {
result = await account.executePaymasterTransaction(
calls,
paymasterDetails,
maxFee,
);
}
} catch (error: any) {
trackApiError(request, ROUTE, "POST", error, 500, {
wallet_address: walletAddress,
});
return NextResponse.json(
{ error: error.message || "Failed to execute transaction" },
{ status: 500 },
);
}

try {
const txReceipt = await account.waitForTransaction(
result.transaction_hash,
);
if (!txReceipt.isSuccess()) {
return NextResponse.json(
{ error: "Transaction reverted on-chain" },
{ status: 500 },
);
}
} catch {
return NextResponse.json(
{
error:
"Transaction submitted but confirmation failed. Please verify status in the explorer.",
transactionHash: result.transaction_hash,
},
{ status: 502 },
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the cited lines
wc -l app/api/starknet/earn/deposit/route.ts
sed -n '1,340p' app/api/starknet/earn/deposit/route.ts

# Search for obvious idempotency/duplicate-submission handling in the codebase
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  -e 'idempot|dedup|duplicate|retry|request.?id|idempotency.?key|transactionHash|transaction_hash|waitForTransaction|prepareVesuDepositCalls|executePaymasterTransaction|deployReadyAccount' \
  app api lib src .

Repository: paycrest/noblocks

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant route and any persistence/state used to prevent duplicate deposits
sed -n '1,340p' app/api/starknet/earn/deposit/route.ts

printf '\n--- related files ---\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  -e 'deposit.*(record|log|idempot|dedup|status|txHash|requestId|nonce)|idempot|request.?id|retry|transaction_hash|waitForTransaction|executePaymasterTransaction|prepareVesuDepositCalls' \
  app/api/starknet/earn -S

Repository: paycrest/noblocks

Length of output: 10356


Add idempotency to the deposit POST This can submit the same deposit twice if the client retries after a lost response; the 502 branch only helps once a transaction hash already exists. Persist an idempotency key or request record before executePaymasterTransaction and short-circuit duplicates.

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

In `@app/api/starknet/earn/deposit/route.ts` around lines 238 - 286, The deposit
POST in route.ts is not idempotent, so retries can submit the same transaction
twice before `account.executePaymasterTransaction` or `deployReadyAccount`
completes. Add a persisted idempotency key or request record in the POST handler
before the transaction call, keyed off the incoming request so duplicate
submissions can be detected and short-circuited. Update the flow around
`deployReadyAccount`, `account.executePaymasterTransaction`, and the
`trackApiError`/`waitForTransaction` branches to return the prior result for
repeated requests instead of sending a second on-chain transaction.

Comment on lines +71 to +82
const { allBalances, refreshBalance } = useBalance();
const { positions, refreshPosition, deposit, withdraw } = useEarnHandler();
const { tokens: availableEarnTokens } = useEarnAvailableTokens();

const tokenDropdownItems = useMemo(
() =>
availableEarnTokens.map((t) => ({
name: t,
imageUrl: `/logos/${t.toLowerCase()}-logo.svg`,
})),
[availableEarnTokens],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files 'app/components/EarnWalletForm.tsx' 'app/**/useEarnAvailableTokens*' 'app/**/useEarnHandler*' 'app/**/useBalance*' 'app/**/useEarn*' | sed 's#^`#-` #'

printf '\n== Outline: EarnWalletForm.tsx ==\n'
ast-grep outline app/components/EarnWalletForm.tsx --view expanded || true

printf '\n== Search for availability and positions sources ==\n'
rg -n "useEarnAvailableTokens|useEarnHandler|availableEarnTokens|positions\\b|setToken\\(" app -S

printf '\n== Locate Paycrest/Starknet token availability logic ==\n'
rg -n "Paycrest|Starknet|advertis|available token|availableTokens|EARN_TOKENS|suppliedBaseUnits" app -S

printf '\n== Read relevant files (small slices) ==\n'
for f in \
  app/components/EarnWalletForm.tsx \
  app/**/useEarnAvailableTokens* \
  app/**/useEarnHandler* \
  app/**/useBalance*; do
  [ -e "$f" ] || continue
  echo
  echo "--- $f ---"
  wc -l "$f"
done

Repository: paycrest/noblocks

Length of output: 43005


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== EarnWalletForm.tsx: token selection and withdraw gating =='
sed -n '70,190p' app/components/EarnWalletForm.tsx
echo
sed -n '400,500p' app/components/EarnWalletForm.tsx

echo
echo '== useEarnHandler.ts: advertised-token logic =='
sed -n '1,70p' app/hooks/useEarnHandler.ts

Repository: paycrest/noblocks

Length of output: 10116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

p = Path('app/components/EarnWalletForm.tsx')
text = p.read_text()
for needle in [
    'const tokenDropdownItems',
    'const withdrawableTokens',
    'const selectableTokens',
    'hasAnySuppliedBalance',
    'setToken(match)',
    'availableEarnTokens.find',
]:
    idx = text.find(needle)
    print(f'\n## {needle} @ {idx}')
    if idx != -1:
        start = max(0, idx - 500)
        end = min(len(text), idx + 900)
        print(text[start:end])

print('\n== useEarnHandler.ts excerpt ==')
p2 = Path('app/hooks/useEarnHandler.ts')
text2 = p2.read_text()
for needle in ['getNetworkTokens("Starknet")', 'const intersection', 'setTokens(intersection.length > 0 ? intersection : EARN_TOKENS)']:
    idx = text2.find(needle)
    print(f'\n## {needle} @ {idx}')
    if idx != -1:
        start = max(0, idx - 300)
        end = min(len(text2), idx + 700)
        print(text2[start:end])
PY

Repository: paycrest/noblocks

Length of output: 9140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== EarnWalletForm.tsx around hasAnySuppliedBalance and submit logic =='
sed -n '150,240p' app/components/EarnWalletForm.tsx
echo
sed -n '240,330p' app/components/EarnWalletForm.tsx
echo
sed -n '330,520p' app/components/EarnWalletForm.tsx

echo
echo '== Search uses of hasAnySuppliedBalance =='
rg -n "hasAnySuppliedBalance" app/components/EarnWalletForm.tsx app -S

Repository: paycrest/noblocks

Length of output: 13222


Include existing supplied tokens in withdraw selection availableEarnTokens gates the withdraw tab, dropdown, and onSelect, so a user with a non-zero position in a token that drops out of the current Starknet list can’t select it or withdraw it. Union the advertised tokens with any token that still has suppliedBaseUnits > 0 before building the withdraw-side list. app/components/EarnWalletForm.tsx:71-82, 161-173, 419-430

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

In `@app/components/EarnWalletForm.tsx` around lines 71 - 82, The withdraw flow is
incorrectly limited to only `availableEarnTokens`, so tokens with an existing
non-zero supplied position can disappear from the withdraw tab and selection.
Update `EarnWalletForm` to union the advertised tokens from
`useEarnAvailableTokens` with any token from current `positions` that still has
`suppliedBaseUnits > 0`, and use that combined list for the withdraw-side
dropdown/tab and the `onSelect` logic. Keep the fix localized around
`tokenDropdownItems`, the withdraw tab rendering, and the withdraw selection
handlers in `EarnWalletForm`.

Comment on lines 266 to +284
const { client } = useSmartWallets();

const showEarnUi = isEarnUiVisible(selectedNetwork.chain.name);
const {
isConsentModalOpen: isEarnConsentModalOpen,
requestEarnAccess,
handleConsentAccepted: handleEarnConsentAccepted,
dismissConsent: dismissEarnConsent,
} = useEarnAccess();

const onEarnAccessAction = (
action: "earn-modal" | "earn-tab" | "earn-hub",
) => {
if (action === "earn-hub" || action === "earn-modal") {
setCurrentView("earn");
}
};

const walletBalanceUsd = crossChainTotal ?? 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File list ==\n'
git ls-files | rg '^(app/components/MobileDropdown\.tsx|app/components/WalletDetails\.tsx)$'

printf '\n== Outline: MobileDropdown.tsx ==\n'
ast-grep outline app/components/MobileDropdown.tsx --view expanded || true

printf '\n== Outline: WalletDetails.tsx ==\n'
ast-grep outline app/components/WalletDetails.tsx --view expanded || true

printf '\n== Relevant occurrences in MobileDropdown.tsx ==\n'
rg -n --context 3 'showEarnUi|currentView|earn-deposit|earn-withdraw|earn-activity-detail|setCurrentView|setSelectedEarnActivity|isEarnUiVisible' app/components/MobileDropdown.tsx

printf '\n== Relevant occurrences in WalletDetails.tsx ==\n'
rg -n --context 3 'sidebarView|showEarnUi|earn-deposit|earn-withdraw|earn-activity-detail|setSidebarView|isEarnUiVisible' app/components/WalletDetails.tsx

Repository: paycrest/noblocks

Length of output: 13857


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== MobileDropdown effects touching network/view state ==\n'
rg -n --context 2 'useEffect\(|selectedNetwork|showEarnUi|currentView|setCurrentView\("wallet"\)|setSelectedEarnActivity\(null\)' app/components/MobileDropdown.tsx

printf '\n== MobileDropdown state declarations for view state ==\n'
sed -n '53,120p' app/components/MobileDropdown.tsx

printf '\n== MobileDropdown close/reset effects ==\n'
sed -n '236,270p' app/components/MobileDropdown.tsx

Repository: paycrest/noblocks

Length of output: 10041


Reset the mobile Earn view when showEarnUi turns false
currentView can stay on earn, earn-deposit, or earn-withdraw after a network change, so those branches stop matching and the sheet body goes empty. Add the same fallback used in WalletDetails.tsx here, and clear any selected Earn activity as part of the reset.

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

In `@app/components/MobileDropdown.tsx` around lines 266 - 284, Reset the
Earn-related mobile sheet state in MobileDropdown when showEarnUi becomes false,
since currentView can otherwise remain stuck on earn, earn-deposit, or
earn-withdraw and render nothing. Add the same fallback pattern used in
WalletDetails for the showEarnUi / currentView handling, and make sure the reset
also clears any selected Earn activity state so the view returns to a valid
non-Earn screen.

Comment on lines +40 to +56
const trimmed = transactionId.trim();
if (!trimmed) {
return { orderId: "", chainId: null, canonicalTransactionId: "" };
}

const match = /^(\d+)-(.+)$/.exec(trimmed);
if (match) {
const chainId = Number(match[1]);
const orderId = match[2].trim();
if (Number.isInteger(chainId) && chainId > 0 && orderId.length > 0) {
return {
orderId,
chainId,
canonicalTransactionId: `${chainId}-${orderId}`,
};
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '\bparseEvmChainPrefixedOrderId\s*\(' --type=ts -C3

Repository: paycrest/noblocks

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and related symbols first.
ast-grep outline app/lib/payment-order-id.ts --view expanded

# Find the helper definition and all references to related parser/formatter helpers.
rg -n "parseEvmChainPrefixedOrderId|isSenderPaymentOrderUuid|resolve.*PaymentOrder|SENDER_ORDER_UUID_RE|payment-order-id" app -S -C 2

# Inspect the target file with line numbers.
cat -n app/lib/payment-order-id.ts | sed -n '1,220p'

Repository: paycrest/noblocks

Length of output: 6090


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the helper implementation and any exported entry points in the file.
cat -n app/lib/payment-order-id.ts | sed -n '1,260p'

# Find all in-repo references to the public helpers from this module.
rg -n "isGatewayOrderId|isSenderPaymentOrderUuid|resolvePaymentOrderId|resolve.*OrderId|canonicalTransactionId" app -S -C 2

Repository: paycrest/noblocks

Length of output: 9695


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the module in small chunks to understand the parsing flow.
sed -n '1,220p' app/lib/payment-order-id.ts | cat -n

# Search for all imports/usages of this module.
rg -n "from ['\"].*payment-order-id['\"]|payment-order-id" . -S -g '!**/node_modules/**'

Repository: paycrest/noblocks

Length of output: 2857


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the cashback route around the parser call and request validation.
sed -n '60,170p' app/api/blockfest/cashback/route.ts | cat -n

Repository: paycrest/noblocks

Length of output: 4585


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any docs or tests that describe accepted transactionId shapes for the cashback endpoint.
rg -n "blockfest cashback|transactionId|parseEvmChainPrefixedOrderId|sender payment order|UUID" app test tests -S -C 2

Repository: paycrest/noblocks

Length of output: 14683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe the parser semantics for a few representative inputs.
python3 - <<'PY'
import re

uuid_re = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', re.I)
prefix_re = re.compile(r'^(\d+)-(.+)$')

samples = [
    "12345678-90ab-4cde-8f01-234567890abc",
    "8453-12345678-90ab-4cde-8f01-234567890abc",
    "0xabc",
    " 12345678-90ab-4cde-8f01-234567890abc ",
]

for s in samples:
    t = s.strip()
    m = prefix_re.match(t)
    if m and int(m.group(1)) > 0 and m.group(2).strip():
        out = (m.group(2).strip(), int(m.group(1)), f"{int(m.group(1))}-{m.group(2).strip()}")
    else:
        out = (t, None, t)
    print(s, "uuid?", bool(uuid_re.match(t)), "->", out)
PY

Repository: paycrest/noblocks

Length of output: 618


Guard UUIDs before the chain-prefix parse ^(\d+)-(.+)$ still misparses sender UUIDs whose first block is all digits (for example 12345678-90ab-4cde-8f01-234567890abc) as { chainId: 12345678, orderId: "90ab-..." }. That breaks lookup for a valid subset of UUID inputs; check isSenderPaymentOrderUuid() before applying the prefix regex.

🧰 Tools
🪛 OpenGrep (1.23.0)

[ERROR] 45-45: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

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

In `@app/lib/payment-order-id.ts` around lines 40 - 56, The prefix parsing in
payment-order-id.ts is too permissive and can misread sender UUIDs as
chain-prefixed order IDs. Update the parsing flow in the transactionId handling
logic to call isSenderPaymentOrderUuid() before applying the /^(\d+)-(.+)$/
chain-prefix regex, and only attempt the chainId/orderId split when the value is
not a sender UUID. Keep the existing canonicalTransactionId and returned shape
in the parse routine unchanged for valid non-UUID inputs.

@5ran6
5ran6 merged commit 48c792f into main Jul 6, 2026
2 checks passed
@5ran6
5ran6 deleted the stable branch July 6, 2026 08:03
@5ran6
5ran6 restored the stable branch July 6, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants