Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2d3109a
feat(referrals): implement referral system with code gen, tracking, a…
sundayonah Nov 18, 2025
0e3de13
fix: coderabbit comments
sundayonah Nov 19, 2025
63dc412
fix: add bottom-up ReferralDashboardView modal in MobileDropdown
sundayonah Nov 19, 2025
f18e8ae
refactor(referral): simplify claim to KYC + $100 tx volume, integrate…
sundayonah Nov 21, 2025
03cea8f
fix: update referral/claim file for consistency
sundayonah Nov 24, 2025
cbbedb9
refactor(referral): update ReferralDashboard and ReferralDashboardVie…
sundayonah Nov 26, 2025
36aa221
refactor(referral): streamline referral claim process with improved w…
sundayonah Dec 1, 2025
d65dff6
refactor(referral): enhance error handling and data processing in cla…
sundayonah Dec 1, 2025
9758c41
refactor(analytics): replace Promise.resolve with after for asynchron…
sundayonah Dec 1, 2025
c7b030a
refactor(referral): consolidate referral code generation and enhance …
sundayonah Dec 1, 2025
f2b546b
fix(referral): update referral transaction threshold from $20 to $100…
sundayonah Dec 1, 2025
06dafbf
feat: complete glitchtip integration with sentry wizard (#297)
sundayonah Dec 17, 2025
4553987
feat: enhance user tracking with server-side event logging (#302)
sundayonah Dec 17, 2025
0c61121
refactor: update next.config.mjs for improved configuration management
chibie Dec 17, 2025
a628ef1
feat: add low-memory build support and disable sourcemaps in next.con…
chibie Dec 18, 2025
f448fcc
Merge branch 'main' into feat/user-referral-and-earnings-program
sundayonah Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ MIXPANEL_PRIVACY_MODE=strict
MIXPANEL_INCLUDE_IP=false
MIXPANEL_INCLUDE_ERROR_STACKS=false
NEXT_PUBLIC_ENABLE_EMAIL_IN_ANALYTICS=false
NEXT_PUBLIC_FEE_RECIPIENT_ADDRESS=

# =============================================================================
# Security
Expand Down
94 changes: 94 additions & 0 deletions app/api/aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import type {
RecipientDetails,
RecipientDetailsWithId,
SavedRecipientsResponse,
ReferralData,
ApiResponse,
SubmitReferralResult,
} from "../types";
import {
trackServerEvent,
Expand Down Expand Up @@ -508,6 +511,97 @@ export const fetchTokens = async (): Promise<APIToken[]> => {
}
};

/**
* Submit a referral code for a new user
*/
export async function submitReferralCode(
code: string,
accessToken?: string,
): Promise<ApiResponse<SubmitReferralResult>> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};

if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}

try {
const response = await axios.post(`/api/referral/submit`, { referral_code: code }, { headers });

if (!response.data?.success) {
return { success: false, error: response.data?.error || response.data?.message || "Failed to submit referral code", status: response.status };
}

return { success: true, data: response.data?.data || response.data } as ApiResponse<SubmitReferralResult>;
} catch (error) {
if (axios.isAxiosError(error)) {
const message = error.response?.data?.message || error.message || "Failed to submit referral code";
return { success: false, error: message, status: error.response?.status };
}
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
}


/**
* Get user's referral data (code, earnings, referral list)
*/
export async function getReferralData(
accessToken?: string,
walletAddress?: string,
): Promise<ApiResponse<ReferralData>> {
const headers: Record<string, string> = {};
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;

const url = walletAddress
? `/api/referral/data?wallet_address=${encodeURIComponent(walletAddress)}`
: `/api/referral/data`;

try {
const response = await axios.get(url, { headers });

if (!response.data?.success) {
return { success: false, error: response.data?.error || "Failed to fetch referral data", status: response.status };
}

return { success: true, data: response.data.data as ReferralData };
} catch (error) {
if (axios.isAxiosError(error)) {
const message = error.response?.data?.message || error.message || "Failed to fetch referral data";
return { success: false, error: message, status: error.response?.status };
}
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
}

/**
* Generate or get user's referral code
*/
export async function generateReferralCode(
accessToken?: string,
): Promise<ApiResponse<{ referral_code?: string; referralCode?: string; code?: string }>> {
const headers: Record<string, string> = {};
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;

try {
const response = await axios.get(`/api/referral/generate-referral-code`, { headers });

if (!response.data?.success) {
return { success: false, error: response.data?.error || "Failed to generate referral code", status: response.status };
}

const payload = response.data;
return { success: true, data: payload.data || payload };
} catch (error) {
if (axios.isAxiosError(error)) {
const message = error.response?.data?.message || error.message || "Failed to generate referral code";
return { success: false, error: message, status: error.response?.status };
}
return { success: false, error: error instanceof Error ? error.message : "Unknown error" };
}
}

/**
* Fetches saved recipients for a wallet address
* @param {string} accessToken - The access token for authentication
Expand Down
162 changes: 162 additions & 0 deletions app/api/internal/credit-wallet/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { NextRequest, NextResponse } from "next/server";
import { supabaseAdmin } from "@/app/lib/supabase";
import * as ethers from "ethers";

export const POST = async (request: NextRequest) => {
const internalAuth = process.env.INTERNAL_API_KEY;
const headerAuth = request.headers.get("x-internal-auth");

if (!internalAuth || headerAuth !== internalAuth) {
return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
}

let body: any;
try {
body = await request.json();
} catch (err) {
return NextResponse.json({ success: false, error: "Invalid JSON body" }, { status: 400 });
}

const {
idempotency_key,
wallet_address,
amount,
currency = "USDC",
referral_id,
reason,
metadata,
} = body || {};

if (!idempotency_key || !wallet_address || typeof amount !== "number") {
return NextResponse.json({ success: false, error: "Missing required fields" }, { status: 400 });
}

try {
// Check for existing credit by idempotency_key
const { data: existing } = await supabaseAdmin
.from("credits")
.select("*")
.eq("idempotency_key", idempotency_key)
.limit(1)
.single();

if (existing) {
if (existing.status === "failed") {
return NextResponse.json(
{ success: false, error: "Existing credit is in failed state" },
{ status: 500 }
);
}
return NextResponse.json({ success: true, data: existing });
}

const now = new Date().toISOString();
const basePayload: any = {
referral_id: referral_id || null,
idempotency_key,
wallet_address: wallet_address.toLowerCase(),
amount_micro: amount,
currency,
status: "pending",
external_tx: null,
reason: reason || "credit",
metadata: metadata || null,
created_at: now,
updated_at: now,
};

// Insert pending record (idempotency_key is UNIQUE in migration). Handle race with select on conflict.
let created: any = null;
try {
const insertRes = await supabaseAdmin
.from("credits")
.insert(basePayload)
.select()
.single();

if (insertRes.error) throw insertRes.error;
created = insertRes.data;
} catch (insErr) {
// If insertion failed due to unique constraint, try to fetch existing row
console.warn("Insert error for credit row, attempting to fetch existing:", (insErr as any)?.message || insErr);
const { data: existing2 } = await supabaseAdmin
.from("credits")
.select("*")
.eq("idempotency_key", idempotency_key)
.limit(1)
.single();

if (existing2) {
return NextResponse.json({ success: true, data: existing2 });
}

console.error("Failed to insert or retrieve existing credit row:", insErr);
return NextResponse.json({ success: false, error: "Failed to create credit record" }, { status: 500 });
}

// If hot wallet config present, perform on-chain ERC20 transfer
const HOT_KEY = process.env.HOT_WALLET_PRIVATE_KEY;
const RPC_URL = process.env.RPC_URL;
const TOKEN_ADDRESS = process.env.TOKEN_CONTRACT_ADDRESS;
const TOKEN_DECIMALS = Number(process.env.TOKEN_DECIMALS ?? 6);

if (HOT_KEY && RPC_URL && TOKEN_ADDRESS) {
try {
const provider = new (ethers as any).providers.JsonRpcProvider(RPC_URL);
const wallet = new (ethers as any).Wallet(HOT_KEY, provider);
const erc20Abi = ["function transfer(address to, uint256 amount) public returns (bool)"];
const contract = new (ethers as any).Contract(TOKEN_ADDRESS, erc20Abi, wallet);

// amount is already in micro-units, convert to BigNumber
const bnAmount = (ethers as any).BigNumber.from(amount.toString());
const tx = await contract.transfer(wallet_address, bnAmount);
const receipt = await tx.wait();

// Update credits row as sent with tx hash
const { error: updateErr } = await supabaseAdmin
.from("credits")
.update({ status: "sent", external_tx: receipt.transactionHash, updated_at: new Date().toISOString() })
.eq("id", created.id);

if (updateErr) {
console.error("Failed to update credit row after transfer:", updateErr);
}

const { data: finalRow } = await supabaseAdmin.from("credits").select("*").eq("id", created.id).single();
return NextResponse.json({ success: true, data: finalRow });
} catch (txErr) {
console.error("Transfer failed:", txErr);
// mark row as failed
try {
await supabaseAdmin
.from("credits")
.update({ status: "failed", error: String((txErr as any)?.message || txErr), updated_at: new Date().toISOString() })
.eq("id", created.id);
} catch (markErr) {
console.error("Failed to mark credit as failed:", markErr);
}
return NextResponse.json({ success: false, error: "Transfer failed" }, { status: 500 });
}
}

// No hot-wallet configured: mark as sent (stub)
try {
const { error: finalErr } = await supabaseAdmin
.from("credits")
.update({ status: "sent", external_tx: "stubbed-credit", updated_at: new Date().toISOString() })
.eq("id", created.id);

if (finalErr) {
console.error("Failed to finalize stub credit row:", finalErr);
}
const { data: finalRow } = await supabaseAdmin.from("credits").select("*").eq("id", created.id).single();
return NextResponse.json({ success: true, data: finalRow });
} catch (finalizeErr) {
console.error("Error finalizing stub credit:", finalizeErr);
return NextResponse.json({ success: false, error: "Internal error finalizing credit" }, { status: 500 });
}
} catch (error) {
console.error("Error in internal credit-wallet:", error);
return NextResponse.json({ success: false, error: "Internal error" }, { status: 500 });
}
};
Loading