-
Notifications
You must be signed in to change notification settings - Fork 49
Feat/onramp implementation #333
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
2108651
cba5f4c
aac898c
c231171
93b02d4
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 |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { | |
| trackApiError, | ||
| trackBusinessEvent, | ||
| } from "@/app/lib/server-analytics"; | ||
| import { isValidEvmAddressCaseInsensitive } from "@/app/lib/validation"; | ||
| import type { | ||
| RecipientDetailsWithId, | ||
| SavedRecipientsResponse, | ||
|
|
@@ -42,20 +43,33 @@ export const GET = withRateLimit(async (request: NextRequest) => { | |
| wallet_address: walletAddress, | ||
| }); | ||
|
|
||
| const { data: recipients, error } = await supabaseAdmin | ||
| // Fetch bank/mobile_money recipients | ||
| const { data: bankRecipients, error: bankError } = await supabaseAdmin | ||
| .from("saved_recipients") | ||
| .select("*") | ||
| .eq("normalized_wallet_address", walletAddress) | ||
| .order("created_at", { ascending: false }); | ||
|
|
||
| if (error) { | ||
| console.error("Supabase query error:", error); | ||
| throw error; | ||
| if (bankError) { | ||
| console.error("Supabase query error:", bankError); | ||
| throw bankError; | ||
| } | ||
|
|
||
| // Fetch wallet recipients | ||
| const { data: walletRecipients, error: walletError } = await supabaseAdmin | ||
| .from("saved_wallet_recipients") | ||
| .select("*") | ||
| .eq("normalized_wallet_address", walletAddress) | ||
| .order("created_at", { ascending: false }); | ||
|
|
||
| if (walletError) { | ||
| console.error("Supabase query error:", walletError); | ||
| throw walletError; | ||
| } | ||
|
|
||
| // Transform database format to frontend format | ||
| const transformedRecipients: RecipientDetailsWithId[] = | ||
| recipients?.map((recipient) => ({ | ||
| // Transform bank/mobile_money recipients | ||
| const transformedBankRecipients: RecipientDetailsWithId[] = | ||
| bankRecipients?.map((recipient) => ({ | ||
| id: recipient.id, | ||
| name: recipient.name, | ||
| institution: recipient.institution, | ||
|
|
@@ -64,6 +78,21 @@ export const GET = withRateLimit(async (request: NextRequest) => { | |
| type: recipient.type, | ||
| })) || []; | ||
|
|
||
| // Transform wallet recipients | ||
| const transformedWalletRecipients: RecipientDetailsWithId[] = | ||
| walletRecipients?.map((recipient) => ({ | ||
| id: recipient.id, | ||
| type: "wallet" as const, | ||
| walletAddress: recipient.recipient_wallet_address, | ||
| name: recipient.name || "", | ||
| })) || []; | ||
|
|
||
| // Combine both types of recipients | ||
| const transformedRecipients: RecipientDetailsWithId[] = [ | ||
| ...transformedBankRecipients, | ||
| ...transformedWalletRecipients, | ||
| ]; | ||
|
|
||
| const response: SavedRecipientsResponse = { | ||
| success: true, | ||
| data: transformedRecipients, | ||
|
|
@@ -124,9 +153,146 @@ export const POST = withRateLimit(async (request: NextRequest) => { | |
| }); | ||
|
|
||
| const body = await request.json(); | ||
| const { name, institution, institutionCode, accountIdentifier, type, currency } = | ||
| const { name, institution, institutionCode, accountIdentifier, type, currency walletAddress: walletAddressFromBody } = | ||
| body; | ||
|
Comment on lines
155
to
157
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: fd "route.ts" -x sh -c 'echo "=== {} ===" && wc -l {} && sed -n "150,160p" {} | cat -n' | grep -A 15 "recipients"Repository: paycrest/noblocks Length of output: 1222 Fix the request-body destructuring syntax. Line 156 is missing a comma between Suggested fix- const { name, institution, institutionCode, accountIdentifier, type, currency walletAddress: walletAddressFromBody } =
- body;
+ const {
+ name,
+ institution,
+ institutionCode,
+ accountIdentifier,
+ type,
+ currency,
+ walletAddress: walletAddressFromBody,
+ } = body;🧰 Tools🪛 Biome (2.4.9)[error] 156-156: expected (parse) 🤖 Prompt for AI Agents |
||
|
|
||
| // Handle wallet recipients (onramp) | ||
| if (type === "wallet") { | ||
| if (!walletAddressFromBody) { | ||
| trackApiError( | ||
| request, | ||
| "/api/v1/recipients", | ||
| "POST", | ||
| new Error("Missing required field: walletAddress"), | ||
| 400, | ||
| ); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: "Missing required field: walletAddress", | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| // Validate wallet address format | ||
| if (!isValidEvmAddressCaseInsensitive(walletAddressFromBody.trim())) { | ||
| trackApiError( | ||
| request, | ||
| "/api/v1/recipients", | ||
| "POST", | ||
| new Error("Invalid wallet address format"), | ||
| 400, | ||
| ); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: "Invalid wallet address format", | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| // Check recipient count limit (100 max per wallet) | ||
| const { count: recipientCount, error: countError } = await supabaseAdmin | ||
| .from("saved_wallet_recipients") | ||
| .select("*", { count: "exact", head: true }) | ||
| .eq("normalized_wallet_address", walletAddress); | ||
|
|
||
| if (countError) { | ||
| console.error("Error checking recipient count:", countError); | ||
| throw countError; | ||
| } | ||
|
|
||
| // If at limit, remove the oldest recipient before adding new one | ||
| if (recipientCount && recipientCount >= 100) { | ||
| const { data: oldestRecipient } = await supabaseAdmin | ||
| .from("saved_wallet_recipients") | ||
| .select("id") | ||
| .eq("normalized_wallet_address", walletAddress) | ||
| .order("created_at", { ascending: true }) | ||
| .limit(1) | ||
| .single(); | ||
|
|
||
| if (oldestRecipient) { | ||
| await supabaseAdmin | ||
| .from("saved_wallet_recipients") | ||
| .delete() | ||
| .eq("id", oldestRecipient.id); | ||
| } | ||
| } | ||
|
|
||
| // Validate name for wallet recipients | ||
| if (!name || !name.trim()) { | ||
| trackApiError( | ||
| request, | ||
| "/api/v1/recipients", | ||
| "POST", | ||
| new Error("Missing required field: name"), | ||
| 400, | ||
| ); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: "Missing required field: name", | ||
| }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
Comment on lines
+196
to
+241
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. Only evict when a new wallet recipient will actually be inserted. Lines 208-222 can delete the oldest saved wallet before Line 226 validates 🤖 Prompt for AI Agents |
||
|
|
||
| // Insert wallet recipient into saved_wallet_recipients table | ||
| const { data, error } = await supabaseAdmin | ||
| .from("saved_wallet_recipients") | ||
| .upsert( | ||
| { | ||
| wallet_address: walletAddress, | ||
| normalized_wallet_address: walletAddress, | ||
| recipient_wallet_address: walletAddressFromBody.trim(), | ||
| normalized_recipient_wallet_address: walletAddressFromBody.toLowerCase().trim(), | ||
| name: name.trim(), | ||
| }, | ||
| { | ||
| onConflict: "normalized_wallet_address,normalized_recipient_wallet_address", | ||
| }, | ||
| ) | ||
| .select() | ||
| .single(); | ||
|
|
||
| if (error) { | ||
| console.error("Supabase insert error:", error); | ||
| throw error; | ||
| } | ||
|
|
||
| // Transform to frontend format | ||
| const transformedRecipient: RecipientDetailsWithId = { | ||
| id: data.id, | ||
| type: "wallet", | ||
| walletAddress: data.recipient_wallet_address, | ||
| name: data.name, | ||
| }; | ||
|
|
||
| const response: SaveRecipientResponse = { | ||
| success: true, | ||
| data: transformedRecipient, | ||
| }; | ||
|
|
||
| // Track successful API response | ||
| const responseTime = Date.now() - startTime; | ||
| trackApiResponse("/api/v1/recipients", "POST", 200, responseTime, { | ||
| wallet_address: walletAddress, | ||
| type: "wallet", | ||
| }); | ||
|
|
||
| // Track business event | ||
| trackBusinessEvent("Recipient Saved", { | ||
| wallet_address: walletAddress, | ||
| type: "wallet", | ||
| }); | ||
|
|
||
| return NextResponse.json(response, { status: 201 }); | ||
| } | ||
|
|
||
| // Handle bank/mobile_money recipients (offramp) | ||
| // Validate request body | ||
| if ( | ||
| !name || | ||
|
|
@@ -343,16 +509,44 @@ export const DELETE = withRateLimit(async (request: NextRequest) => { | |
| recipient_id: recipientId, | ||
| }); | ||
|
|
||
| // Delete the recipient (RLS policies ensure only owner can delete) | ||
| const { error: deleteError } = await supabaseAdmin | ||
| .from("saved_recipients") | ||
| .delete() | ||
| // Check which table the recipient is in by trying to find it first | ||
| const { data: walletRecipient, error: walletQueryError } = await supabaseAdmin | ||
| .from("saved_wallet_recipients") | ||
| .select("id") | ||
| .eq("id", recipientId) | ||
| .eq("normalized_wallet_address", walletAddress); | ||
| .eq("normalized_wallet_address", walletAddress) | ||
| .maybeSingle(); | ||
|
|
||
| // Handle query errors (except "no rows found" which is expected) | ||
| if (walletQueryError && walletQueryError.code !== "PGRST116") { | ||
| console.error("Error querying wallet recipient:", walletQueryError); | ||
| throw walletQueryError; | ||
| } | ||
|
|
||
| if (deleteError) { | ||
| console.error("Error deleting recipient:", deleteError); | ||
| throw deleteError; | ||
| if (walletRecipient) { | ||
| // Delete from saved_wallet_recipients | ||
| const { error: walletDeleteError } = await supabaseAdmin | ||
| .from("saved_wallet_recipients") | ||
| .delete() | ||
| .eq("id", recipientId) | ||
| .eq("normalized_wallet_address", walletAddress); | ||
|
|
||
| if (walletDeleteError) { | ||
| console.error("Error deleting wallet recipient:", walletDeleteError); | ||
| throw walletDeleteError; | ||
| } | ||
| } else { | ||
| // Delete from saved_recipients (bank/mobile_money) | ||
| const { error: bankDeleteError } = await supabaseAdmin | ||
| .from("saved_recipients") | ||
| .delete() | ||
| .eq("id", recipientId) | ||
| .eq("normalized_wallet_address", walletAddress); | ||
|
|
||
| if (bankDeleteError) { | ||
| console.error("Error deleting recipient:", bankDeleteError); | ||
| throw bankDeleteError; | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const response = { | ||
|
|
||
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.
Verify recipient data shape before key generation.
The key generation logic assumes that
walletAddressexists whentype === "wallet"and thatinstitutionCode/accountIdentifierexist for bank/mobile_money types. If the runtime data doesn't match the type guards, this could produce malformed keys like"wallet-undefined".Consider adding defensive checks or validation before key generation to handle potentially malformed data from localStorage.
🔎 Suggested defensive handling
const existingKeys = new Set( existingRecipients.map((r) => { if (r.type === "wallet") { + if (!r.walletAddress) { + console.warn("Wallet recipient missing walletAddress", r); + return `wallet-invalid-${r.id}`; + } return `wallet-${r.walletAddress}`; } else { + if (!r.institutionCode || !r.accountIdentifier) { + console.warn("Bank/mobile_money recipient missing required fields", r); + return `${r.type}-invalid-${r.id}`; + } return `${r.institutionCode}-${r.accountIdentifier}`; } }), );🤖 Prompt for AI Agents