From 8c86728927eef23f928497e897b239531ceb4bdf Mon Sep 17 00:00:00 2001 From: Assad Isah Date: Sun, 30 Aug 2026 17:29:03 +0100 Subject: [PATCH 1/2] fix: resolve compilation error in test_double_mint_rejected test --- onchain/Cargo.lock | 2 ++ onchain/contracts/stellar_hunts_nft/src/test.rs | 8 +------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/onchain/Cargo.lock b/onchain/Cargo.lock index 16ed8f8e..05ba5cf4 100644 --- a/onchain/Cargo.lock +++ b/onchain/Cargo.lock @@ -1429,6 +1429,8 @@ name = "stellar-hunts-receiver" version = "0.1.0" dependencies = [ "soroban-sdk", + "stellar-hunts-nft", + "stellar-hunts-types", ] [[package]] diff --git a/onchain/contracts/stellar_hunts_nft/src/test.rs b/onchain/contracts/stellar_hunts_nft/src/test.rs index dfb3db41..1e27830e 100644 --- a/onchain/contracts/stellar_hunts_nft/src/test.rs +++ b/onchain/contracts/stellar_hunts_nft/src/test.rs @@ -84,13 +84,7 @@ fn test_double_mint_rejected() { game.mint(&nft_id, &r, &crate::Levels::Easy); // Second mint must fail (already-has-badge error). let should_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.init( - &admin, - &game, - &String::from_str(&env, &long_uri), - &String::from_str(&env, "StellarHuntsBadge"), - &String::from_str(&env, "SHB"), - ); + game.mint(&nft_id, &r, &crate::Levels::Easy); })); assert!(should_panic.is_err()); } From 9d8b8ad3d6190ec2ea56e5f26ec86d432d4ebbc2 Mon Sep 17 00:00:00 2001 From: chemicalcommando Date: Sun, 30 Aug 2026 18:11:35 +0100 Subject: [PATCH 2/2] fix: add unique constraints, abort stale requests, reload loop protection, and reward claim idempotency --- backend/src/auth/entities/user.entity.ts | 2 +- .../reward/entities/reward-claim.entity.ts | 2 + frontend/app/error.js | 10 +- frontend/app/global-error.tsx | 10 +- frontend/hooks/useReferral.js | 288 ++++++++++-------- 5 files changed, 177 insertions(+), 135 deletions(-) diff --git a/backend/src/auth/entities/user.entity.ts b/backend/src/auth/entities/user.entity.ts index 43cdf3c8..5fc16d18 100644 --- a/backend/src/auth/entities/user.entity.ts +++ b/backend/src/auth/entities/user.entity.ts @@ -33,7 +33,7 @@ export class User { @Column({ default: true }) isActive: boolean; - @Column({ nullable: true }) + @Column({ unique: true, nullable: true }) lastLoginAt: Date; @CreateDateColumn() diff --git a/backend/src/reward/entities/reward-claim.entity.ts b/backend/src/reward/entities/reward-claim.entity.ts index fd930dde..4a003877 100644 --- a/backend/src/reward/entities/reward-claim.entity.ts +++ b/backend/src/reward/entities/reward-claim.entity.ts @@ -13,6 +13,8 @@ import { Reward } from './reward.entity'; @Entity('reward_claims') @Index(['userId', 'rewardId'], { unique: true }) export class RewardClaim { + @Column({ unique: true, nullable: true }) + claimKey?: string; @ApiProperty({ description: 'Unique identifier for the reward claim' }) @PrimaryGeneratedColumn('uuid') id: string; diff --git a/frontend/app/error.js b/frontend/app/error.js index bae77854..d26b267d 100644 --- a/frontend/app/error.js +++ b/frontend/app/error.js @@ -7,7 +7,15 @@ import AnimatedBlurBackground from "@/components/AnimatedBlurBackground"; export default function Error() { const reload = () => { - window.location.reload(); + if (typeof window !== 'undefined') { + const count = parseInt(sessionStorage.getItem('error_retry_count') || '0', 10); + if (count >= 3) { + alert('Multiple errors occurred. Please try returning home or contacting support.'); + return; + } + sessionStorage.setItem('error_retry_count', (count + 1).toString()); + window.location.reload(); + } }; return ( diff --git a/frontend/app/global-error.tsx b/frontend/app/global-error.tsx index 5d94dc54..7c00ecea 100644 --- a/frontend/app/global-error.tsx +++ b/frontend/app/global-error.tsx @@ -5,7 +5,15 @@ import { Home, RefreshCcw } from "lucide-react"; export default function GlobalError({ error, reset }) { const reload = () => { - window.location.reload(); + if (typeof window !== 'undefined') { + const count = parseInt(sessionStorage.getItem('error_retry_count') || '0', 10); + if (count >= 3) { + alert('Multiple errors occurred. Please try returning home or contacting support.'); + return; + } + sessionStorage.setItem('error_retry_count', (count + 1).toString()); + window.location.reload(); + } }; return ( diff --git a/frontend/hooks/useReferral.js b/frontend/hooks/useReferral.js index 4f030ce1..e09a240d 100644 --- a/frontend/hooks/useReferral.js +++ b/frontend/hooks/useReferral.js @@ -1,133 +1,157 @@ -import { useState, useEffect } from "react"; -import axios from "axios"; - -export const useReferral = (userId = null) => { - const [referralStats, setReferralStats] = useState({ - totalInvites: 0, - activeUsers: 0, - totalRewards: 0, - totalXPEarned: 0, - nextMilestone: "" - }); - - const [invitedUsers, setInvitedUsers] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (userId) { - fetchReferralData(userId); - } - }, [userId]); - - // Generate referral link for current user - const generateReferralLink = (userId) => { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://nft-hunt.com"; - return `${baseUrl}/ref/${userId}`; - }; - - // Fetch referral data - const fetchReferralData = async (userId) => { - if (!userId) return; - - setLoading(true); - setError(null); - - try { - const response = await axios.get(`/api/referrals/${userId}`, { - withCredentials: true - }); - - setReferralStats(response.data.stats); - setInvitedUsers(response.data.invitedUsers); - } catch (err) { - console.error("Failed to fetch referral data:", err); - setError("Failed to load referral data"); - } finally { - setLoading(false); - } - }; - - // Track new referral - const trackReferral = async (referrerId, newUserId) => { - try { - await axios.post("/api/referrals/track", { - referrerId, - newUserId - }, { - withCredentials: true - }); - - // Refresh referral data - await fetchReferralData(referrerId); - } catch (err) { - console.error("Failed to track referral:", err); - } - }; - - // Get reward tier info - const getRewardTier = (totalInvites) => { - if (totalInvites >= 50) return { tier: "Mythic", reward: "Mythic NFT", color: "pink" }; - if (totalInvites >= 25) return { tier: "Legendary", reward: "Legendary NFT", color: "yellow" }; - if (totalInvites >= 10) return { tier: "Epic", reward: "Epic NFT", color: "purple" }; - if (totalInvites >= 5) return { tier: "Rare", reward: "Rare NFT", color: "green" }; - return { tier: "Common", reward: "Common NFT", color: "gray" }; - }; - - // Calculate progress to next milestone - const getProgressToNextMilestone = (currentInvites) => { - const milestones = [5, 10, 25, 50]; - const nextMilestone = milestones.find(m => m > currentInvites) || 50; - const progress = (currentInvites / nextMilestone) * 100; - - return { - current: currentInvites, - next: nextMilestone, - progress: Math.min(progress, 100), - remaining: nextMilestone - currentInvites - }; - }; - - // Share referral link - const shareReferral = async (referralLink) => { - if (navigator.share) { - try { - await navigator.share({ - title: "Join StellarHunts!", - text: "I'm playing this amazing StellarHunts game. Join me and earn exclusive rewards!", - url: referralLink - }); - return true; - } catch (err) { - console.error("Error sharing:", err); - return false; - } - } - return false; - }; - - // Copy referral link to clipboard - const copyReferralLink = async (referralLink) => { - try { - await navigator.clipboard.writeText(referralLink); - return true; - } catch (err) { - console.error("Failed to copy:", err); - return false; - } - }; - - return { - referralStats, - invitedUsers, - loading, - error, - generateReferralLink, - fetchReferralData, - trackReferral, - getRewardTier, - getProgressToNextMilestone, - shareReferral, - copyReferralLink - }; +import { useState, useEffect } from "react"; +import axios from "axios"; + +export const useReferral = (userId = null) => { + const [referralStats, setReferralStats] = useState({ + totalInvites: 0, + activeUsers: 0, + totalRewards: 0, + totalXPEarned: 0, + nextMilestone: "" + }); + + const [invitedUsers, setInvitedUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const controller = new AbortController(); + if (userId) { + const fetchReferralData = async (userId) => { + if (!userId) return; + setLoading(true); + setError(null); + try { + const response = await axios.get(`/api/referrals/${userId}`, { + withCredentials: true, + signal: controller.signal + }); + setReferralStats(response.data.stats); + setInvitedUsers(response.data.invitedUsers); + } catch (err) { + if (!axios.isCancel(err)) { + console.error("Failed to fetch referral data:", err); + setError("Failed to load referral data"); + } + } finally { + setLoading(false); + } + }; + fetchReferralData(userId); + } + return () => { + controller.abort(); + }; + }, [userId]); + + // Generate referral link for current user + const generateReferralLink = (userId) => { + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://nft-hunt.com"; + return `${baseUrl}/ref/${userId}`; + }; + + // Fetch referral data + const fetchReferralData = async (userId) => { + if (!userId) return; + + setLoading(true); + setError(null); + + try { + const response = await axios.get(`/api/referrals/${userId}`, { + withCredentials: true + }); + + setReferralStats(response.data.stats); + setInvitedUsers(response.data.invitedUsers); + } catch (err) { + console.error("Failed to fetch referral data:", err); + setError("Failed to load referral data"); + } finally { + setLoading(false); + } + }; + + // Track new referral + const trackReferral = async (referrerId, newUserId) => { + try { + await axios.post("/api/referrals/track", { + referrerId, + newUserId + }, { + withCredentials: true + }); + + // Refresh referral data + await fetchReferralData(referrerId); + } catch (err) { + console.error("Failed to track referral:", err); + } + }; + + // Get reward tier info + const getRewardTier = (totalInvites) => { + if (totalInvites >= 50) return { tier: "Mythic", reward: "Mythic NFT", color: "pink" }; + if (totalInvites >= 25) return { tier: "Legendary", reward: "Legendary NFT", color: "yellow" }; + if (totalInvites >= 10) return { tier: "Epic", reward: "Epic NFT", color: "purple" }; + if (totalInvites >= 5) return { tier: "Rare", reward: "Rare NFT", color: "green" }; + return { tier: "Common", reward: "Common NFT", color: "gray" }; + }; + + // Calculate progress to next milestone + const getProgressToNextMilestone = (currentInvites) => { + const milestones = [5, 10, 25, 50]; + const nextMilestone = milestones.find(m => m > currentInvites) || 50; + const progress = (currentInvites / nextMilestone) * 100; + + return { + current: currentInvites, + next: nextMilestone, + progress: Math.min(progress, 100), + remaining: nextMilestone - currentInvites + }; + }; + + // Share referral link + const shareReferral = async (referralLink) => { + if (navigator.share) { + try { + await navigator.share({ + title: "Join StellarHunts!", + text: "I'm playing this amazing StellarHunts game. Join me and earn exclusive rewards!", + url: referralLink + }); + return true; + } catch (err) { + console.error("Error sharing:", err); + return false; + } + } + return false; + }; + + // Copy referral link to clipboard + const copyReferralLink = async (referralLink) => { + try { + await navigator.clipboard.writeText(referralLink); + return true; + } catch (err) { + console.error("Failed to copy:", err); + return false; + } + }; + + return { + referralStats, + invitedUsers, + loading, + error, + generateReferralLink, + fetchReferralData, + trackReferral, + getRewardTier, + getProgressToNextMilestone, + shareReferral, + copyReferralLink + }; }; \ No newline at end of file