Skip to content

Conversation

@sundayonah
Copy link
Collaborator

@sundayonah sundayonah commented Dec 30, 2025

Description

This PR implements a comprehensive wallet migration system to transition existing Noblocks users from Smart Contract Wallets (SCW) to EIP-7702-enabled Externally Owned Accounts (EOA). This migration ensures wallet portability and aligns with the latest Ethereum standards while maintaining a seamless user experience.

Key Features Implemented:

  1. EIP-7702 Integration for New Users

    • Automatic provisioning of EIP-7702-compatible EOAs for all new users via Privy's embedded wallet system
    • Configuration updated in privy-config.ts with createOnLogin: "all-users" to ensure all new accounts are EIP-7702-ready
  2. Migration Banner & Modal System

    • Persistent, non-dismissible banner that appears for existing users with old SCWs
    • Banner displays migration deadline (June 6th, 2025) and prompts users to start migration
    • Modal interface (WalletTransferApprovalModal) displays all tokens across all supported networks
    • Real-time balance calculation across Base, Polygon, BNB Chain, Arbitrum, and Lisk networks
  3. Multi-Chain Balance Aggregation

    • Fetches balances from all supported networks in parallel (optimized for performance)
    • Groups tokens by chain for organized display
    • Calculates total USD value using current exchange rates
  4. Gasless Migration Transactions

    • Batched token transfers per chain using Account Abstraction (AA)
    • Integration with Biconomy paymaster for gasless transactions
    • Single transaction per chain to transfer all tokens from old SCW to new EOA
    • Handles ERC-20 token transfers with proper encoding and validation

References

Checklist

  • I have added documentation and tests for new/changed/fixed 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

Added Files:

  • app/api/v1/wallets/deprecate/route.ts
  • app/api/v1/wallets/migration-status/route.ts
  • app/components/WalletMigrationBanner.tsx
  • app/components/WalletMigrationModal.tsx
  • app/components/WalletMigrationSuccessModal.tsx
  • app/components/WalletTransferApprovalModal.tsx
  • app/context/MigrationContext.tsx
  • app/hooks/useEIP7702Account.ts
  • supabase/migrations/create_wallet_migrations.sql
  • Migration images in public/images/

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

Summary by CodeRabbit

  • New Features

    • Guided wallet migration flow with modals, approval UI, real-time progress, and success screens.
    • Migration banner prompting eligible users to start migration.
    • New endpoint to check migration status.
  • Chores

    • Backend migration ledger added to audit and track migrations with access controls.
  • Bug Fixes / Reliability

    • Wallet deprecation endpoint now preserves KYC data when possible and includes rollback/error handling for safer migrations.

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

- Added WalletMigrationBanner and WalletMigrationModal components for user migration prompts.
- Introduced WalletTransferApprovalModal for handling wallet transfer approvals.
- Created hooks for managing EIP-7702 account setup and migration status checks.
- Implemented backend API routes for deprecating old wallets and checking migration status.
- Updated context to include migration-related components and logic.
- Enhanced UI with new images and improved user feedback during migration processes.
@sundayonah sundayonah requested a review from chibie as a code owner December 30, 2025 07:23
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

Adds end-to-end wallet migration: new backend routes to deprecate wallets and query migration status, a DB ledger for migrations with RLS, client hooks/context to detect migration need, UI components to guide and run multi-chain fund transfers, and integration with Privy for batched gasless transfers and post-migration upsert/rollback logic.

Changes

Cohort / File(s) Summary
Migration API Routes
app/api/v1/wallets/deprecate/route.ts, app/api/v1/wallets/migration-status/route.ts
New POST /wallets/deprecate: auth, rate-limit, analytics, atomic-like deprecate + upsert with rollback, KYC migration (non-fatal errors), emits metrics; new GET /wallets/migration-status: returns migrationCompleted/status/hasSmartWallet with error handling.
Migration UI Components
app/components/WalletMigrationBanner.tsx, app/components/WalletMigrationModal.tsx, app/components/WalletTransferApprovalModal.tsx, app/components/WalletMigrationSuccessModal.tsx
New client components (banner, overview modal, transfer approval flow, success modal) implementing multi-step UX, Framer Motion + Headless UI, per-chain balance display, approve → batched transfers → progress and success UI.
Context, Hooks & Integration
app/context/MigrationContext.tsx, app/context/index.ts, app/hooks/useEIP7702Account.ts
MigrationBannerWrapper conditionally renders banner; hooks expose EOA/SCW, signer, isReady, and needsMigration/isChecking using Privy and balance context with fallback/error handling.
App Layout & Exports
app/components/AppLayout.tsx, app/components/index.ts
Injects MigrationBannerWrapper into AppLayout header; exports added for WalletMigrationBanner and MigrationBannerWrapper.
Database Migration
supabase/migrations/create_wallet_migrations.sql
New wallet_migrations table with constraints, indexes, updated_at trigger, RLS enabled, service-role full access policy and users-can-read-own policy, unique per user.
Minor Formatting
app/lib/privy-config.ts
Whitespace/import formatting change only; no behavior change.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI_Banner as WalletMigrationBanner
    participant UI_Modal as WalletMigrationModal
    participant UI_Transfer as WalletTransferApprovalModal
    participant Privy as Privy Client
    participant Backend as Backend API
    participant SC as Old Smart Contract Wallet

    User->>UI_Banner: clicks "Start migration"
    UI_Banner->>UI_Modal: open overview modal
    User->>UI_Modal: clicks "Approve migration"
    UI_Modal->>UI_Transfer: open transfer approval modal

    UI_Transfer->>Privy: fetch per-chain balances for SC
    Privy-->>UI_Transfer: return tokens & balances
    User->>UI_Transfer: confirm transfer

    par Per-chain batch transfer
      UI_Transfer->>Privy: switch to chain N
      Privy-->>UI_Transfer: chain switched
      UI_Transfer->>Privy: submit batched gasless transfer tx
      Privy->>SC: execute transfer from old SCW -> new EOA
      SC-->>Privy: tx receipt
    end

    UI_Transfer->>Backend: POST /api/v1/wallets/deprecate (old, new, txHashes)
    Backend->>Backend: deprecate old, upsert new, attempt KYC migration (log non-fatal)
    Backend-->>UI_Transfer: success / partial-success
    UI_Transfer->>User: show success modal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • chibie
  • Dprof-in-tech

Poem

🐰 I hopped from SCW to shiny EOA,
Batches of tokens rolled out with glee,
A banner, a modal, and gasless delight,
DB ledger ticks, and KYC sailed free—
Hooray, new wallets ready for me!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: implementing a wallet migration feature with modals and context support.
Description check ✅ Passed The PR description comprehensively covers the purpose, key features, references, and implementation details. However, the Testing and Checklist sections lack completion details.
Linked Issues check ✅ Passed The PR implementation addresses all major acceptance criteria from issue #316: EIP-7702 integration with Privy config, persistent migration banner/modal UI, multi-chain balance calculation, single-transaction gasless transfers via Biconomy, and wallet deprecation with database tracking.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the wallet migration feature. Minor whitespace-only formatting changes in privy-config.ts are within scope as part of configuration updates.
✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (4)
supabase/migrations/create_wallet_migrations.sql (1)

16-23: Redundant index on privy_user_id.

The unique constraint unique_user_migration on privy_user_id (lines 17-19) already creates an implicit index. The explicit index idx_wallet_migrations_user_id (lines 22-23) is redundant.

🔎 Proposed fix
 -- Unique constraint to prevent duplicate migrations per user
 alter table public.wallet_migrations 
 add constraint unique_user_migration 
 unique (privy_user_id);

--- Indexes for efficient lookups
-create index if not exists idx_wallet_migrations_user_id 
-  on public.wallet_migrations(privy_user_id);
-
+-- Indexes for efficient lookups (privy_user_id index covered by unique constraint)
 create index if not exists idx_wallet_migrations_old_scw 
   on public.wallet_migrations(old_scw_address);
app/components/WalletMigrationBanner.tsx (1)

39-46: Consider extracting the migration deadline to a shared constant.

The deadline "6th June, 2025" is hardcoded in two places (desktop line 43 and mobile line 83). Extracting this to a constant or configuration would make updates easier and ensure consistency.

🔎 Proposed fix
+"use client";
+import Image from "next/image";
+import React, { useState } from "react";
+import { motion } from "framer-motion";
+import WalletMigrationModal from "./WalletMigrationModal";
+
+const MIGRATION_DEADLINE = "6th June, 2025";
+
 export const WalletMigrationBanner = () => {

Then use {MIGRATION_DEADLINE} in both banner variants.

Also applies to: 80-85

app/components/WalletTransferApprovalModal.tsx (1)

61-96: Missing cleanup may cause state updates on unmounted component.

If the modal is closed while fetchAllChainBalances is running, the async function continues and calls setAllChainBalances / setIsFetchingBalances on an unmounted component, potentially causing React warnings or memory leaks.

🔎 Proposed fix
     useEffect(() => {
         if (!isOpen || !oldAddress) return;
+        let cancelled = false;

         const fetchAllChainBalances = async () => {
             setIsFetchingBalances(true);
             const balancesByChain: Record<string, Record<string, number>> = {};

             for (const network of networks) {
                 try {
+                    if (cancelled) return;
                     const publicClient = createPublicClient({
                         chain: network.chain,
                         transport: http(),
                     });

                     const result = await fetchWalletBalance(
                         publicClient,
                         oldAddress
                     );

                     const hasBalance = Object.values(result.balances).some(b => b > 0);
                     if (hasBalance) {
                         balancesByChain[network.chain.name] = result.balances;
                     }
                 } catch (error) {
                     console.error(`Error fetching balances for ${network.chain.name}:`, error);
                 }
             }

+            if (cancelled) return;
             setAllChainBalances(balancesByChain);
             setIsFetchingBalances(false);
         };

         fetchAllChainBalances();
+        return () => { cancelled = true; };
     }, [isOpen, oldAddress]);
app/hooks/useEIP7702Account.ts (1)

76-76: Effect dependency wallets.length may miss wallet updates.

Using wallets.length as a dependency means the effect won't re-run if wallet properties change (e.g., address updates) while the array length remains the same. This could cause stale wallet data.

🔎 Proposed fix
-    }, [authenticated, user?.id, wallets.length]);
+    }, [authenticated, user?.id, wallets]);

Note: If wallets array reference changes too frequently causing performance issues, consider using a more specific dependency like wallets.map(w => w.address).join(',') or memoizing the embedded wallet lookup.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a605397 and a2c30cd.

⛔ Files ignored due to path filters (4)
  • public/images/checkmark-circle.svg is excluded by !**/*.svg
  • public/images/desktop-eip-migration.png is excluded by !**/*.png
  • public/images/mobile-eip-migration.png is excluded by !**/*.png
  • public/images/wallet.png is excluded by !**/*.png
📒 Files selected for processing (13)
  • app/api/v1/wallets/deprecate/route.ts
  • app/api/v1/wallets/migration-status/route.ts
  • app/components/AppLayout.tsx
  • app/components/WalletMigrationBanner.tsx
  • app/components/WalletMigrationModal.tsx
  • app/components/WalletMigrationSuccessModal.tsx
  • app/components/WalletTransferApprovalModal.tsx
  • app/components/index.ts
  • app/context/MigrationContext.tsx
  • app/context/index.ts
  • app/hooks/useEIP7702Account.ts
  • app/lib/privy-config.ts
  • supabase/migrations/create_wallet_migrations.sql
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

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

Applied to files:

  • app/components/AppLayout.tsx
🧬 Code graph analysis (4)
app/components/WalletMigrationBanner.tsx (1)
app/components/index.ts (1)
  • WalletMigrationBanner (57-57)
app/api/v1/wallets/migration-status/route.ts (1)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/v1/wallets/deprecate/route.ts (2)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/components/AppLayout.tsx (2)
app/context/MigrationContext.tsx (1)
  • MigrationBannerWrapper (5-19)
app/context/index.ts (1)
  • MigrationBannerWrapper (15-15)
🔇 Additional comments (11)
app/lib/privy-config.ts (2)

1-1: EIP-7702 configuration correctly enables new user provisioning.

Line 16 appropriately sets createOnLogin: "all-users" to provision new users as EIP-7702-enabled EOAs per the PR objectives. All imported chains are utilized in the supportedChains array (line 28), and the required networks for migration (Base, Polygon, Arbitrum, Lisk, and BNB Chain) are included.


1-1: Verify that baseSepolia removal was intentional.

The PR commit message mentions "removes an unused chain reference (baseSepolia)", but the AI summary describes this as a "whitespace-only modification." Please confirm that baseSepolia was removed intentionally and is no longer needed for this configuration.

app/context/index.ts (1)

15-15: LGTM!

The export follows the established pattern in this file.

app/components/index.ts (1)

57-57: LGTM!

The export follows the established pattern in this file.

app/components/AppLayout.tsx (1)

15-27: LGTM!

The MigrationBannerWrapper is properly imported and positioned in the header area after the existing NoticeBanner, following the established layout pattern.

app/context/MigrationContext.tsx (1)

5-19: LGTM!

Clean conditional rendering with clear early-return logic. The component appropriately hides itself during the checking phase and when migration isn't needed.

app/components/WalletMigrationBanner.tsx (1)

7-104: LGTM!

The component properly handles responsive design with separate desktop/mobile layouts, and the modal integration is clean. The fixed positioning with top-16 correctly places the banner below the navbar.

app/components/WalletMigrationSuccessModal.tsx (1)

12-98: LGTM!

The modal implementation follows good practices with proper accessibility attributes, smooth animations via Framer Motion, and correct Headless UI Dialog usage.

app/components/WalletMigrationModal.tsx (2)

14-29: LGTM!

The modal state management and transition logic between the two modals is clean. The 300ms delay for the animation transition is a reasonable approach to ensure smooth UX.


31-211: LGTM!

The modal UI implementation is well-structured with proper accessibility attributes (aria-label), responsive design patterns, and consistent dark mode support. The AnimatePresence wrapper correctly handles exit animations.

app/components/WalletTransferApprovalModal.tsx (1)

276-282: Partial migration continues silently on chain errors.

When a chain transfer fails, the error is caught and a toast is shown, but the loop continues to process remaining chains. This can leave users in a partial migration state where some chains succeeded and others failed, yet the deprecation API is still called.

Consider tracking failed chains and either retrying, stopping the migration, or informing the user more explicitly about partial failures before proceeding with deprecation.

Comment on lines 30 to 36
} catch (error) {
console.error("Error checking migration status:", error);
return NextResponse.json({
error: "Internal server error",
migrationCompleted: false
}, { status: 500 });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent response shape on error.

The success response includes migrationCompleted, status, and hasSmartWallet, but the error response only includes error and migrationCompleted. Consider returning a consistent shape for easier client-side handling.

🔎 Proposed fix
     } catch (error) {
         console.error("Error checking migration status:", error);
         return NextResponse.json({
             error: "Internal server error",
-            migrationCompleted: false
+            migrationCompleted: false,
+            status: "unknown",
+            hasSmartWallet: false
         }, { status: 500 });
     }
🤖 Prompt for AI Agents
In app/api/v1/wallets/migration-status/route.ts around lines 30 to 36, the error
branch returns a different JSON shape than the success response; update the
catch block to return the same keys as the success response (migrationCompleted,
status, hasSmartWallet) plus an error message so the client always receives a
consistent shape. Populate migrationCompleted:false, status:"error" (or the same
enum/string used on success), hasSmartWallet:false (or null if that better
matches types), and include error: error?.message || String(error) in the JSON,
and keep the 500 status code.

…y config

- Corrected the return type documentation in the TokensContext function.
- Removed baseSepolia from the supportedChains list in PrivyClientConfig as it is no longer needed.
@sundayonah sundayonah force-pushed the feat/eip-7702-wallet-migration-clean branch from a2c30cd to 8971509 Compare December 30, 2025 07:35
… validation

- Implemented JWT verification for secure access to the wallet deprecation endpoint.
- Added checks to ensure the authenticated user matches the provided userId and wallet address.
- Improved error handling for unauthorized access and token validation.
- Introduced rollback mechanisms for database operations to maintain data integrity during migration.
- Updated KYC data migration logic to allow for partial success reporting.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
app/components/WalletTransferApprovalModal.tsx (1)

173-179: Contradictory zero-balance migration logic remains unresolved.

The code at lines 173-179 explicitly handles zero-balance migration ("No tokens to migrate, but deprecating old wallet..."), but line 479 disables the button when tokens.length === 0. Users with zero balance cannot initiate the wallet deprecation they need.

🔎 Proposed fix
                             <button
                                 onClick={handleApproveTransfer}
-                                disabled={isProcessing || isLoading || isFetchingBalances || tokens.length === 0}
+                                disabled={isProcessing || isLoading || isFetchingBalances}
                                 className="w-full rounded-xl bg-lavender-500 px-6 py-3.5 text-base font-semibold text-white hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
                             >
-                                {isProcessing ? progress || "Processing migration..." : isFetchingBalances ? "Loading balances..." : "Approve transfer"}
+                                {isProcessing ? progress || "Processing migration..." : isFetchingBalances ? "Loading balances..." : tokens.length === 0 ? "Deprecate wallet" : "Approve transfer"}
                             </button>

Also applies to: 479-479

🧹 Nitpick comments (3)
app/hooks/useEIP7702Account.ts (1)

107-167: Potential stale closure issue with smartWalletBalance.

The smartWalletBalance value is captured at line 132 before the async fetch call. If allBalances.smartWallet?.total changes during the API call, the fallback logic (lines 154-155 and 159-160) will use the stale value. While this is unlikely to cause significant issues in practice, it's worth noting.

app/components/WalletTransferApprovalModal.tsx (1)

68-89: Consider parallel balance fetching for better UX.

The current implementation fetches balances sequentially for each network. For 5+ networks, this can result in noticeable delays. Consider using Promise.allSettled for parallel fetching.

🔎 Proposed optimization
-            // Fetch balances for each supported network
-            for (const network of networks) {
-                try {
-                    const publicClient = createPublicClient({
-                        chain: network.chain,
-                        transport: http(),
-                    });
-
-                    const result = await fetchWalletBalance(
-                        publicClient,
-                        oldAddress
-                    );
-
-                    // Only include chains with non-zero balances
-                    const hasBalance = Object.values(result.balances).some(b => b > 0);
-                    if (hasBalance) {
-                        balancesByChain[network.chain.name] = result.balances;
-                    }
-                } catch (error) {
-                    console.error(`Error fetching balances for ${network.chain.name}:`, error);
-                }
-            }
+            // Fetch balances for all networks in parallel
+            const results = await Promise.allSettled(
+                networks.map(async (network) => {
+                    const publicClient = createPublicClient({
+                        chain: network.chain,
+                        transport: http(),
+                    });
+                    const result = await fetchWalletBalance(publicClient, oldAddress);
+                    return { chainName: network.chain.name, result };
+                })
+            );
+
+            for (const settled of results) {
+                if (settled.status === 'fulfilled') {
+                    const { chainName, result } = settled.value;
+                    const hasBalance = Object.values(result.balances).some(b => b > 0);
+                    if (hasBalance) {
+                        balancesByChain[chainName] = result.balances;
+                    }
+                }
+            }
supabase/migrations/create_wallet_migrations.sql (1)

22-23: Redundant index on privy_user_id.

The UNIQUE constraint on privy_user_id (line 17-19) already creates an implicit B-tree index. The explicit idx_wallet_migrations_user_id index is redundant.

🔎 Proposed fix
 -- Indexes for efficient lookups
-create index if not exists idx_wallet_migrations_user_id 
-  on public.wallet_migrations(privy_user_id);
-
 create index if not exists idx_wallet_migrations_old_scw 
   on public.wallet_migrations(old_scw_address);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8971509 and f33f6eb.

📒 Files selected for processing (5)
  • app/api/v1/wallets/deprecate/route.ts
  • app/api/v1/wallets/migration-status/route.ts
  • app/components/WalletTransferApprovalModal.tsx
  • app/hooks/useEIP7702Account.ts
  • supabase/migrations/create_wallet_migrations.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/api/v1/wallets/deprecate/route.ts
  • app/api/v1/wallets/migration-status/route.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

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

Applied to files:

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

Applied to files:

  • app/components/WalletTransferApprovalModal.tsx
  • app/hooks/useEIP7702Account.ts
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/hooks/useEIP7702Account.ts
🧬 Code graph analysis (1)
app/components/WalletTransferApprovalModal.tsx (4)
app/mocks.ts (1)
  • networks (44-88)
app/context/index.ts (3)
  • useBalance (3-3)
  • useTokens (10-10)
  • useNetwork (2-2)
app/hooks/useActualTheme.ts (1)
  • useActualTheme (4-25)
app/utils.ts (3)
  • fetchWalletBalance (460-506)
  • formatCurrency (70-83)
  • shortenAddress (577-586)
🔇 Additional comments (6)
app/hooks/useEIP7702Account.ts (2)

62-72: Previous issue addressed - LGTM.

The early return case when embeddedWallet is missing now properly calls setAccount with isReady: true, preventing the perpetual loading state identified in the previous review.


22-86: LGTM - Well-structured hook with proper state management.

The useEIP7702Account hook correctly handles all edge cases (unauthenticated, no smart wallet, no embedded wallet) and sets isReady: true appropriately in each branch.

app/components/WalletTransferApprovalModal.tsx (2)

225-230: Precision fix applied correctly, but inherent limitation remains.

The parseUnits usage is an improvement over Math.floor, but token.amount is still a Number which has ~15-17 significant digits. For tokens with 18 decimals and values with many significant figures, minor precision loss is possible. This would require refactoring fetchWalletBalance to return raw BigInt balances for a complete fix.


30-52: Well-structured component with comprehensive wallet migration handling.

The component properly integrates with Privy smart wallets, handles multi-chain transfers, and provides good user feedback through progress indicators and toast notifications.

supabase/migrations/create_wallet_migrations.sql (2)

2-14: Solid table design with proper constraints.

Good use of CHECK constraints for address validation and status enumeration. The nullable new_eoa_address allows for pending migration states.


64-69: RLS policy correctly validates JWT sub claim mapping.

The policy properly restricts reads to rows where privy_user_id matches the JWT sub claim from Supabase's current_setting('request.jwt.claims'). The Privy JWT verification in middleware (line 86 of middleware.ts) confirms that payload.sub contains the privy_user_id. In Supabase's authenticated context, JWT claims are always available, so the NULL handling is safe—a missing claim would simply return no rows, which is the correct security default.

Note: The wallet_migrations table is defined with RLS but is not currently populated by any application code. If this is intentional for future use, the table is correctly configured. If it should be actively used, integration with the wallet deprecation workflow may be needed.

Comment on lines +114 to +120
if (tokenMeta?.address) {
// Get CNGN rate for this chain if needed
const chainRate = symbol.toUpperCase() === "CNGN" ? rate : undefined;
let usdValue = balanceNum;
if (chainRate) {
usdValue = balanceNum / chainRate;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CNGN rate applied incorrectly across chains.

The rate from useCNGNRate is tied to selectedNetwork, but this logic applies it to CNGN tokens on all chains. If the user has CNGN on multiple networks with different rates, the USD values will be incorrect for chains other than the selected network.

🔎 Suggested approach

Consider fetching CNGN rates per chain, or filtering CNGN conversion to only apply when chainName === selectedNetwork.chain.name:

                     // Get CNGN rate for this chain if needed
-                    const chainRate = symbol.toUpperCase() === "CNGN" ? rate : undefined;
+                    // Only apply rate if this chain matches selectedNetwork (rate is network-specific)
+                    const chainRate = symbol.toUpperCase() === "CNGN" && chainName === selectedNetwork.chain.name ? rate : undefined;
                     let usdValue = balanceNum;
                     if (chainRate) {
                         usdValue = balanceNum / chainRate;
+                    } else if (symbol.toUpperCase() === "CNGN") {
+                        // For CNGN on other chains, we don't have the rate - show raw balance or fetch rate
+                        usdValue = balanceNum; // Consider fetching per-chain rate
                     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/components/WalletTransferApprovalModal.tsx around lines 114 to 120, the
code applies the single `rate` from useCNGNRate to any token with symbol "CNGN"
regardless of chain, causing incorrect USD values for CNGN on chains other than
the currently selected network; update the logic to only apply the CNGN
conversion when the token's chain matches the selectedNetwork (e.g., chainName
=== selectedNetwork.chain.name) or fetch/lookup a per-chain CNGN rate keyed by
chain and use that rate for the conversion, otherwise leave usdValue as
balanceNum.

Comment on lines +309 to +314
toast.success("🎉 Migration Complete!", {
description: hasTokens
? `${allTxHashes.length} token(s) successfully migrated to your EOA`
: "Old wallet deprecated successfully",
duration: 5000,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading success message count.

allTxHashes.length represents the number of successful chain transactions, not the total number of tokens migrated. This could confuse users.

🔎 Proposed fix
             toast.success("🎉 Migration Complete!", {
                 description: hasTokens
-                    ? `${allTxHashes.length} token(s) successfully migrated to your EOA`
+                    ? `Assets from ${allTxHashes.length} chain(s) successfully migrated to your EOA`
                     : "Old wallet deprecated successfully",
                 duration: 5000,
             });
🤖 Prompt for AI Agents
In app/components/WalletTransferApprovalModal.tsx around lines 309 to 314, the
toast success message uses allTxHashes.length (number of successful chain
transactions) which is misleading as it does not reflect the actual number of
tokens migrated; change the message to use the true token count (e.g., a
migratedTokens.length or computed totalTokensMigrated variable) or otherwise
compute the number of tokens migrated from the migration result, and update the
description string (with proper pluralization) so it reports the token count
(and optionally also show transaction count like "X tokens migrated (Y txs)").

<>
<AnimatePresence>
{isOpen && (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Dialog can be closed during processing via backdrop click.

The close button is properly disabled during isProcessing, but clicking the backdrop still calls onClose. This could confuse users if they accidentally dismiss the modal mid-migration.

🔎 Proposed fix
-                    <Dialog open={isOpen} onClose={onClose} className="relative z-50">
+                    <Dialog open={isOpen} onClose={isProcessing ? () => {} : onClose} className="relative z-50">
📝 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
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<Dialog open={isOpen} onClose={isProcessing ? () => {} : onClose} className="relative z-50">
🤖 Prompt for AI Agents
In app/components/WalletTransferApprovalModal.tsx around line 339, the Dialog's
onClose currently allows backdrop/escape clicks to close the modal during
processing; change the onClose handler so it ignores close requests while
isProcessing is true (i.e., replace onClose directly with a wrapper that returns
early when isProcessing) so backdrop/escape clicks become no-ops during
processing; keep the close button disabled as-is and ensure any other internal
close triggers also call the same guarded onClose wrapper.

Comment on lines +43 to +46
-- Trigger to automatically update updated_at
create trigger wallet_migrations_update_updated_at
before update on public.wallet_migrations
for each row execute function public.update_wallet_migrations_updated_at();
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add DROP TRIGGER IF EXISTS for idempotent migrations.

The trigger creation will fail if the migration runs twice (e.g., in development or rollback scenarios). Add a drop statement for idempotency.

🔎 Proposed fix
 -- Trigger to automatically update updated_at
+drop trigger if exists wallet_migrations_update_updated_at on public.wallet_migrations;
 create trigger wallet_migrations_update_updated_at
 before update on public.wallet_migrations
 for each row execute function public.update_wallet_migrations_updated_at();
📝 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
-- Trigger to automatically update updated_at
create trigger wallet_migrations_update_updated_at
before update on public.wallet_migrations
for each row execute function public.update_wallet_migrations_updated_at();
-- Trigger to automatically update updated_at
drop trigger if exists wallet_migrations_update_updated_at on public.wallet_migrations;
create trigger wallet_migrations_update_updated_at
before update on public.wallet_migrations
for each row execute function public.update_wallet_migrations_updated_at();
🤖 Prompt for AI Agents
In supabase/migrations/create_wallet_migrations.sql around lines 43 to 46, the
CREATE TRIGGER will fail if the migration is run more than once; make the
migration idempotent by adding a DROP TRIGGER IF EXISTS statement immediately
before the CREATE TRIGGER (drop the trigger on public.wallet_migrations with the
same name), then keep the existing CREATE TRIGGER line; ensure the DROP
statement targets the correct schema/table and ends with a semicolon so the
migration can be safely re-run.

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.

EIP-7702 Integration and Migration

2 participants