-
Notifications
You must be signed in to change notification settings - Fork 47
feat: implement wallet migration feature with modals and context support #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
699bf8e
8971509
f33f6eb
25453bc
9805535
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,176 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { supabaseAdmin } from "@/app/lib/supabase"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { withRateLimit } from "@/app/lib/rate-limit"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { trackApiRequest, trackApiResponse, trackApiError } from "@/app/lib/server-analytics"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { verifyJWT } from "@/app/lib/jwt"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DEFAULT_PRIVY_CONFIG } from "@/app/lib/config"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const POST = withRateLimit(async (request: NextRequest) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const startTime = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 1: Verify authentication token | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const authHeader = request.headers.get("Authorization"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = authHeader?.replace("Bearer ", ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!token) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", new Error("Unauthorized"), 401); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Unauthorized" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 401 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let authenticatedUserId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const jwtResult = await verifyJWT(token, DEFAULT_PRIVY_CONFIG); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticatedUserId = jwtResult.payload.sub; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!authenticatedUserId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", new Error("Invalid token"), 401); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Invalid token" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 401 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (jwtError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", jwtError as Error, 401); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Invalid or expired token" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 401 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const walletAddress = request.headers.get("x-wallet-address")?.toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = await request.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { oldAddress, newAddress, txHash, userId } = body; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!walletAddress || !oldAddress || !newAddress || !userId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", new Error("Missing required fields"), 400); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Missing required fields" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 2: Verify userId matches authenticated user (CRITICAL SECURITY FIX) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (userId !== authenticatedUserId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", new Error("Unauthorized: userId mismatch"), 403); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Unauthorized" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 403 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 3: Verify wallet addresses match | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (newAddress.toLowerCase() !== walletAddress) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", new Error("Wallet address mismatch"), 403); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Wallet address mismatch" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 403 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiRequest(request, "/api/v1/wallets/deprecate", "POST", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wallet_address: walletAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| old_address: oldAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_address: newAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 4: Atomic database operations with rollback on failure | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ensure old (SCW) wallet exists and mark as deprecated (upsert so we insert if never saved to DB) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = new Date().toISOString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error: deprecateError } = await supabaseAdmin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("wallets") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .upsert( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address: oldAddress.toLowerCase(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id: userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wallet_type: "smart_contract", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "deprecated", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deprecated_at: now, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migration_completed: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migration_tx_hash: txHash, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updated_at: now, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { onConflict: "address,user_id" } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (deprecateError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", deprecateError, 500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw deprecateError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create or update new EOA wallet record | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error: upsertError } = await supabaseAdmin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("wallets") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .upsert({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address: newAddress.toLowerCase(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user_id: userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wallet_type: "eoa", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "active", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| created_at: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+104
to
+113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Second upsert is missing The first upsert (Line 83-97) correctly specifies Proposed fix const { error: upsertError } = await supabaseAdmin
.from("wallets")
.upsert({
address: newAddress.toLowerCase(),
user_id: userId,
wallet_type: "eoa",
status: "active",
- created_at: new Date().toISOString(),
- });
+ updated_at: new Date().toISOString(),
+ }, { onConflict: "address,user_id" });🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EOA wallet upsert missing onConflict causes duplicate rowsMedium Severity The second |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (upsertError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Rollback: Restore old wallet status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await supabaseAdmin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("wallets") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .update({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "active", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deprecated_at: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migration_completed: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migration_tx_hash: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("address", oldAddress.toLowerCase()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("user_id", userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", upsertError, 500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw upsertError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+115
to
+130
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rollback failure is silently ignored. If the rollback update on Lines 117-126 fails (e.g., network issue, DB outage), the error is never logged or surfaced. The code proceeds to throw Proposed fix if (upsertError) {
// Rollback: Restore old wallet status
- await supabaseAdmin
+ const { error: rollbackError } = await supabaseAdmin
.from("wallets")
.update({
status: "active",
deprecated_at: null,
migration_completed: false,
migration_tx_hash: null,
})
.eq("address", oldAddress.toLowerCase())
.eq("user_id", userId);
+ if (rollbackError) {
+ console.error("Rollback failed after upsert error:", rollbackError);
+ }
+
trackApiError(request, "/api/v1/wallets/deprecate", "POST", upsertError, 500);
throw upsertError;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Migrate KYC data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error: kycError } = await supabaseAdmin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("kyc_data") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .update({ wallet_address: newAddress.toLowerCase() }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("wallet_address", oldAddress.toLowerCase()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .eq("user_id", userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (kycError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("KYC migration error:", kycError); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Return partial success - wallet migrated but KYC migration failed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This is better than rolling back the entire migration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const responseTime = Date.now() - startTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiResponse("/api/v1/wallets/deprecate", "POST", 200, responseTime, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wallet_address: walletAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migration_successful: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kyc_migration_failed: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: "Wallet migrated but KYC migration failed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kycMigrationFailed: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const responseTime = Date.now() - startTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiResponse("/api/v1/wallets/deprecate", "POST", 200, responseTime, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wallet_address: walletAddress, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migration_successful: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ success: true, message: "Wallet migrated successfully" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Error deprecating wallet:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const responseTime = Date.now() - startTime; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trackApiError(request, "/api/v1/wallets/deprecate", "POST", error as Error, 500, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response_time_ms: responseTime, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { success: false, error: "Internal server error" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { supabaseAdmin } from "@/app/lib/supabase"; | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| try { | ||
| const { searchParams } = new URL(request.url); | ||
| const userId = searchParams.get("userId"); | ||
|
|
||
| if (!userId) { | ||
| return NextResponse.json({ error: "User ID required" }, { status: 400 }); | ||
| } | ||
|
Comment on lines
+4
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No authentication or authorization — any caller can query any user's migration status. The 🤖 Prompt for AI Agents |
||
|
|
||
| // Check if user has completed migration | ||
| const { data, error } = await supabaseAdmin | ||
| .from("wallets") | ||
| .select("migration_completed, status, wallet_type") | ||
| .eq("user_id", userId) | ||
| .eq("wallet_type", "smart_contract") | ||
| .single(); | ||
|
Comment on lines
+13
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== Finding SQL migration files ==="
fd -e sql
echo ""
echo "=== Checking for CREATE TABLE statements in migrations ==="
rg -n "CREATE TABLE" --type sql -A 10
echo ""
echo "=== Checking for wallets table definition ==="
rg -n "wallets" --type sql -B 2 -A 5Repository: paycrest/noblocks Length of output: 2959 🏁 Script executed: #!/bin/bash
echo "=== Content of wallet_migrations.sql ==="
cat supabase/migrations/create_wallet_migrations.sql
echo ""
echo "=== Full content of migration-status route ==="
cat app/api/v1/wallets/migration-status/route.ts
echo ""
echo "=== Searching for 'wallets' table reference elsewhere ==="
rg -n "\.from\(['\"]wallets['\"]" --type tsRepository: paycrest/noblocks Length of output: 6642 Create the The code queries a non-existent
The PGRST205 error handling suggests this was known, but the route will fail unless the missing table is created or the query is corrected. Multiple other routes ( 🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Migration status endpoint lacks authentication checkMedium Severity The |
||
|
|
||
| // Handle specific error codes | ||
| if (error) { | ||
| // PGRST116 = no rows found (user has no smart wallet) - this is OK | ||
| if (error.code === "PGRST116") { | ||
| return NextResponse.json({ | ||
| migrationCompleted: false, | ||
| status: "unknown", | ||
| hasSmartWallet: false | ||
| }); | ||
| } | ||
|
|
||
| // PGRST205 = table not found in schema cache (migration not run yet) | ||
| if (error.code === "PGRST205") { | ||
| console.warn("⚠️ Wallets table not found in schema cache. Migration may not be applied yet."); | ||
| return NextResponse.json({ | ||
| migrationCompleted: false, | ||
| status: "unknown", | ||
| hasSmartWallet: true, // Assume true to show banner | ||
| error: "Database schema not ready" | ||
| }, { status: 200 }); // Return 200 so frontend doesn't break | ||
| } | ||
|
|
||
| // For other errors, log and return safe fallback | ||
| console.error("Database query error:", error); | ||
| return NextResponse.json({ | ||
| migrationCompleted: false, | ||
| status: "unknown", | ||
| hasSmartWallet: true, // Assume true to show banner on error | ||
| error: error.message | ||
| }, { status: 200 }); // Return 200 so frontend doesn't break | ||
|
Comment on lines
+32
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning HTTP 200 for all database errors masks failures from monitoring. While returning 200 prevents frontend breakage, it also makes it impossible to set up alerts on error rates. Consider returning appropriate 5xx status codes and handling them gracefully on the client side, or at minimum emit structured error metrics/logs that can be monitored independently. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| migrationCompleted: data?.migration_completed ?? false, | ||
| status: data?.status ?? "unknown", | ||
| hasSmartWallet: !!data | ||
| }); | ||
| } catch (error: any) { | ||
| // Handle connection errors (DNS, network, etc.) | ||
| const errorMessage = error?.message || String(error); | ||
| const isConnectionError = | ||
| errorMessage.includes("ENOTFOUND") || | ||
| errorMessage.includes("fetch failed") || | ||
| errorMessage.includes("ECONNREFUSED") || | ||
| errorMessage.includes("ETIMEDOUT"); | ||
|
|
||
| if (isConnectionError) { | ||
| console.warn("⚠️ Database connection error, returning fallback response:", errorMessage); | ||
| return NextResponse.json({ | ||
| migrationCompleted: false, | ||
| status: "unknown", | ||
| hasSmartWallet: true, // Assume true to show banner if DB is down | ||
| error: "Database temporarily unavailable" | ||
| }, { status: 200 }); // Return 200 so frontend doesn't break | ||
| } | ||
|
|
||
| console.error("Error checking migration status:", error); | ||
| return NextResponse.json({ | ||
| error: error instanceof Error ? error.message : "Internal server error", | ||
| migrationCompleted: false, | ||
| status: "error", | ||
| hasSmartWallet: true // Assume true to show banner on error | ||
| }, { status: 200 }); // Return 200 so frontend doesn't break | ||
| } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
txHashis not validated but used as migration proof.txHashis destructured from the body but omitted from the required-fields check on Line 48. It's then stored asmigration_tx_hashon Line 93. If a caller omitstxHash, the migration record will be created with anulltransaction hash, undermining auditability.Proposed fix
🤖 Prompt for AI Agents