solanapolis 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +518 -0
- package/bin/solanapolis.js +197 -0
- package/convex/_generated/api.d.ts +175 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/agent/conversation.ts +352 -0
- package/convex/agent/embeddingsCache.ts +110 -0
- package/convex/agent/memory.ts +450 -0
- package/convex/agent/schema.ts +53 -0
- package/convex/aiChat.ts +54 -0
- package/convex/aiTown/agent.ts +382 -0
- package/convex/aiTown/agentDescription.ts +27 -0
- package/convex/aiTown/agentInputs.ts +155 -0
- package/convex/aiTown/agentOperations.ts +178 -0
- package/convex/aiTown/conversation.ts +395 -0
- package/convex/aiTown/conversationMembership.ts +38 -0
- package/convex/aiTown/game.ts +371 -0
- package/convex/aiTown/ids.ts +32 -0
- package/convex/aiTown/inputHandler.ts +9 -0
- package/convex/aiTown/inputs.ts +25 -0
- package/convex/aiTown/insertInput.ts +20 -0
- package/convex/aiTown/location.ts +32 -0
- package/convex/aiTown/main.ts +154 -0
- package/convex/aiTown/movement.ts +189 -0
- package/convex/aiTown/player.ts +310 -0
- package/convex/aiTown/playerDescription.ts +35 -0
- package/convex/aiTown/schema.ts +79 -0
- package/convex/aiTown/world.ts +65 -0
- package/convex/aiTown/worldMap.ts +74 -0
- package/convex/chat.ts +79 -0
- package/convex/constants.ts +78 -0
- package/convex/convex.config.ts +6 -0
- package/convex/crons.ts +89 -0
- package/convex/engine/abstractGame.ts +199 -0
- package/convex/engine/historicalObject.ts +355 -0
- package/convex/engine/schema.ts +56 -0
- package/convex/http.ts +36 -0
- package/convex/init.ts +110 -0
- package/convex/messages.ts +53 -0
- package/convex/npcCarAgents.ts +415 -0
- package/convex/schema.ts +61 -0
- package/convex/streaming.ts +23 -0
- package/convex/testing.ts +202 -0
- package/convex/tsconfig.json +18 -0
- package/convex/util/FastIntegerCompression.ts +221 -0
- package/convex/util/assertNever.ts +4 -0
- package/convex/util/asyncMap.ts +20 -0
- package/convex/util/compression.ts +71 -0
- package/convex/util/geometry.ts +132 -0
- package/convex/util/isSimpleObject.ts +11 -0
- package/convex/util/llm.ts +724 -0
- package/convex/util/minheap.ts +38 -0
- package/convex/util/object.ts +22 -0
- package/convex/util/sleep.ts +3 -0
- package/convex/util/types.ts +33 -0
- package/convex/util/xxhash.ts +228 -0
- package/convex/world.ts +257 -0
- package/data/animations/campfire.json +45 -0
- package/data/animations/gentlesparkle.json +37 -0
- package/data/animations/gentlesplash.json +61 -0
- package/data/animations/gentlewaterfall.json +61 -0
- package/data/animations/windmill.json +78 -0
- package/data/characters.ts +121 -0
- package/data/convertMap.js +74 -0
- package/data/gentle.js +330 -0
- package/data/spritesheets/f1.ts +75 -0
- package/data/spritesheets/f2.ts +75 -0
- package/data/spritesheets/f3.ts +75 -0
- package/data/spritesheets/f4.ts +75 -0
- package/data/spritesheets/f5.ts +75 -0
- package/data/spritesheets/f6.ts +75 -0
- package/data/spritesheets/f7.ts +75 -0
- package/data/spritesheets/f8.ts +75 -0
- package/data/spritesheets/p1.ts +59 -0
- package/data/spritesheets/p2.ts +59 -0
- package/data/spritesheets/p3.ts +59 -0
- package/data/spritesheets/player.ts +59 -0
- package/data/spritesheets/types.ts +26 -0
- package/eslint.config.mjs +37 -0
- package/next.config.ts +7 -0
- package/package.json +85 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/helius-icon.svg +84 -0
- package/public/helius-logo.svg +85 -0
- package/public/next.svg +1 -0
- package/public/plane.glb +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/clear-city.ts +74 -0
- package/scripts/seed-wallets.ts +185 -0
- package/scripts/setup-webhook.ts +73 -0
- package/src/app/api/auth/callback/route.ts +6 -0
- package/src/app/api/auth/link-wallet/route.ts +6 -0
- package/src/app/api/auth/phantom/route.ts +6 -0
- package/src/app/api/broadcast-position/route.ts +59 -0
- package/src/app/api/leaderboard/route.ts +85 -0
- package/src/app/api/network-stats/route.ts +86 -0
- package/src/app/api/parcel-reward/route.ts +181 -0
- package/src/app/api/queue-status/route.ts +30 -0
- package/src/app/api/snapshots/route.ts +37 -0
- package/src/app/api/transactions/enhanced/route.ts +57 -0
- package/src/app/api/treasury/route.ts +83 -0
- package/src/app/api/wallet/[address]/balances/route.ts +124 -0
- package/src/app/api/wallet/[address]/identity/route.ts +32 -0
- package/src/app/api/wallet/[address]/route.ts +216 -0
- package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
- package/src/app/api/wallets/route.ts +68 -0
- package/src/app/api/webhooks/helius/route.ts +76 -0
- package/src/app/auth/callback/page.tsx +29 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +39 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/components/AITownNPCs.tsx +206 -0
- package/src/components/ActivityFeed.tsx +189 -0
- package/src/components/AuthPanel.tsx +163 -0
- package/src/components/BeachScene.tsx +280 -0
- package/src/components/Building.tsx +138 -0
- package/src/components/CesiumFlight.tsx +1768 -0
- package/src/components/CesiumGlobe.tsx +616 -0
- package/src/components/CitizenCard.tsx +442 -0
- package/src/components/CitizenCardModal.tsx +153 -0
- package/src/components/CityGrid.tsx +313 -0
- package/src/components/CityLandmarks.tsx +427 -0
- package/src/components/CityScene.tsx +1289 -0
- package/src/components/CitySlotsBadge.tsx +68 -0
- package/src/components/CockpitHUD.tsx +460 -0
- package/src/components/ConvexWrapper.tsx +19 -0
- package/src/components/DubaiDistrict.tsx +630 -0
- package/src/components/FlightMiniMap.tsx +133 -0
- package/src/components/GameChat.tsx +383 -0
- package/src/components/GameHUD.tsx +393 -0
- package/src/components/Ground.tsx +14 -0
- package/src/components/HowItWorksModal.tsx +251 -0
- package/src/components/IngestionBanner.tsx +123 -0
- package/src/components/InstancedBuildings.tsx +316 -0
- package/src/components/InstancedCars.tsx +504 -0
- package/src/components/InstancedCityPlanes.tsx +259 -0
- package/src/components/InstancedHouses.tsx +246 -0
- package/src/components/InstancedLampPosts.tsx +201 -0
- package/src/components/InstancedResidentCars.tsx +357 -0
- package/src/components/InstancedRoadDashes.tsx +42 -0
- package/src/components/InstancedSkyscrapers.tsx +434 -0
- package/src/components/InstancedTrees.tsx +67 -0
- package/src/components/LeaderboardPanel.tsx +136 -0
- package/src/components/MultiplayerPlanes.tsx +128 -0
- package/src/components/NetworkStats.tsx +83 -0
- package/src/components/NewBuildingSpotlight.tsx +93 -0
- package/src/components/ParcelChallengeBanner.tsx +242 -0
- package/src/components/ParcelReward.tsx +191 -0
- package/src/components/Park.tsx +42 -0
- package/src/components/PhantomWrapper.tsx +22 -0
- package/src/components/PixelStreamViewer.tsx +335 -0
- package/src/components/PlaneMode.tsx +190 -0
- package/src/components/PlayerCar.tsx +211 -0
- package/src/components/PlayerPlane.tsx +255 -0
- package/src/components/ProjectileRenderer.tsx +249 -0
- package/src/components/QueueStatusBanner.tsx +86 -0
- package/src/components/RealPlayerTags.tsx +82 -0
- package/src/components/SceneLighting.tsx +382 -0
- package/src/components/SelectionBeam.tsx +59 -0
- package/src/components/SwapPanel.tsx +104 -0
- package/src/components/SwapParticles.tsx +237 -0
- package/src/components/TreasureGate.tsx +505 -0
- package/src/components/WalletPanel.tsx +421 -0
- package/src/components/WalletSearch.tsx +244 -0
- package/src/components/WelcomeOverlay.tsx +135 -0
- package/src/components/WindowTooltip.tsx +498 -0
- package/src/context/AuthContext.tsx +230 -0
- package/src/lib/bot-detection.ts +125 -0
- package/src/lib/building-math.ts +136 -0
- package/src/lib/building-shader.ts +253 -0
- package/src/lib/car-paths.ts +244 -0
- package/src/lib/car-system.ts +182 -0
- package/src/lib/city-constants.ts +29 -0
- package/src/lib/city-slots.ts +35 -0
- package/src/lib/city-zoning.ts +64 -0
- package/src/lib/collision-map.ts +147 -0
- package/src/lib/day-night.ts +252 -0
- package/src/lib/export-card.ts +28 -0
- package/src/lib/helius-webhook.ts +90 -0
- package/src/lib/helius.ts +74 -0
- package/src/lib/house-shader.ts +119 -0
- package/src/lib/mock-data.ts +56 -0
- package/src/lib/multiplayer-manager.ts +329 -0
- package/src/lib/plane-physics.ts +66 -0
- package/src/lib/player-car.ts +147 -0
- package/src/lib/player-plane.ts +200 -0
- package/src/lib/projectile-system.ts +272 -0
- package/src/lib/skyscraper-types.ts +52 -0
- package/src/lib/sound-engine.ts +464 -0
- package/src/lib/supabase-admin.ts +9 -0
- package/src/lib/supabase.ts +8 -0
- package/src/lib/swap-events.ts +70 -0
- package/src/middleware.ts +37 -0
- package/src/types/phantom.d.ts +16 -0
- package/src/types/wallet.ts +20 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { WalletBuilding } from "@/types/wallet";
|
|
5
|
+
import { useAuth } from "@/context/AuthContext";
|
|
6
|
+
|
|
7
|
+
interface WelcomeOverlayProps {
|
|
8
|
+
onExplore: () => void;
|
|
9
|
+
onWalletSubmit: (wallet: WalletBuilding) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function WelcomeOverlay({
|
|
13
|
+
onExplore,
|
|
14
|
+
onWalletSubmit,
|
|
15
|
+
}: WelcomeOverlayProps) {
|
|
16
|
+
const { profile } = useAuth();
|
|
17
|
+
const [address, setAddress] = useState("");
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
const hasAutoSubmitted = useRef(false);
|
|
21
|
+
|
|
22
|
+
// Auto-submit when user has a verified wallet address
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!profile?.wallet_address || hasAutoSubmitted.current || loading) return;
|
|
25
|
+
hasAutoSubmitted.current = true;
|
|
26
|
+
setAddress(profile.wallet_address);
|
|
27
|
+
|
|
28
|
+
(async () => {
|
|
29
|
+
setLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
try {
|
|
32
|
+
const wallet = await fetchWallet(profile.wallet_address!);
|
|
33
|
+
onWalletSubmit(wallet);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
}, [profile?.wallet_address]);
|
|
42
|
+
|
|
43
|
+
async function fetchWallet(walletAddress: string): Promise<WalletBuilding> {
|
|
44
|
+
const res = await fetch(`/api/wallet/${walletAddress}`);
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const body = await res.json().catch(() => null);
|
|
47
|
+
throw new Error(body?.error ?? `Wallet not found`);
|
|
48
|
+
}
|
|
49
|
+
const stats = await res.json();
|
|
50
|
+
return {
|
|
51
|
+
address: stats.address,
|
|
52
|
+
txnCount: stats.txnCount,
|
|
53
|
+
walletAgeDays: stats.walletAgeDays,
|
|
54
|
+
volumeTraded: stats.volumeTraded,
|
|
55
|
+
feesPaid: stats.feesPaid,
|
|
56
|
+
ingestionStatus: stats.ingestionStatus,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
const trimmed = address.trim();
|
|
63
|
+
if (!trimmed) return;
|
|
64
|
+
|
|
65
|
+
setLoading(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
const wallet = await fetchWallet(trimmed);
|
|
69
|
+
onWalletSubmit(wallet);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
72
|
+
} finally {
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="absolute inset-0 z-20 flex items-center justify-center bg-[#0a0a12]/90 backdrop-blur-sm">
|
|
79
|
+
<div className="max-w-sm w-full px-4 sm:px-6 text-center space-y-6">
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
<div className="flex items-center justify-center gap-2.5">
|
|
82
|
+
<img src="/helius-icon.svg" alt="Helius" className="w-7 h-7 sm:w-8 sm:h-8" />
|
|
83
|
+
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight" style={{ color: "#E35930" }}>
|
|
84
|
+
Solanapolis
|
|
85
|
+
</h1>
|
|
86
|
+
</div>
|
|
87
|
+
<p className="text-sm text-white/40">
|
|
88
|
+
The Solana City — powered by Helius
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<form onSubmit={handleSubmit} className="space-y-3">
|
|
93
|
+
<input
|
|
94
|
+
type="text"
|
|
95
|
+
placeholder="Wallet address"
|
|
96
|
+
value={address}
|
|
97
|
+
onChange={(e) => setAddress(e.target.value)}
|
|
98
|
+
disabled={loading}
|
|
99
|
+
className="w-full px-4 py-2.5 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/25 font-mono text-xs focus:outline-none focus:ring-1 focus:ring-[#E35930]/50 focus:border-[#E35930] transition-colors disabled:opacity-50"
|
|
100
|
+
/>
|
|
101
|
+
<button
|
|
102
|
+
type="submit"
|
|
103
|
+
disabled={loading || !address.trim()}
|
|
104
|
+
className="w-full px-4 py-2.5 bg-[#E35930]/80 hover:bg-[#E35930] rounded-lg text-white text-sm font-medium transition-colors disabled:opacity-50"
|
|
105
|
+
>
|
|
106
|
+
{loading ? (
|
|
107
|
+
<span className="flex items-center justify-center gap-2">
|
|
108
|
+
<span className="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
109
|
+
Looking up wallet...
|
|
110
|
+
</span>
|
|
111
|
+
) : (
|
|
112
|
+
"Look up wallet"
|
|
113
|
+
)}
|
|
114
|
+
</button>
|
|
115
|
+
</form>
|
|
116
|
+
|
|
117
|
+
{error && <p className="text-red-400/80 text-xs">{error}</p>}
|
|
118
|
+
|
|
119
|
+
<div className="flex items-center gap-3">
|
|
120
|
+
<div className="flex-1 h-px bg-white/10" />
|
|
121
|
+
<span className="text-white/20 text-xs">or</span>
|
|
122
|
+
<div className="flex-1 h-px bg-white/10" />
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<button
|
|
126
|
+
onClick={onExplore}
|
|
127
|
+
disabled={loading}
|
|
128
|
+
className="w-full px-4 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-white/60 text-sm transition-colors disabled:opacity-50 cursor-pointer"
|
|
129
|
+
>
|
|
130
|
+
Explore city
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
interface TokenInfo {
|
|
6
|
+
mint: string;
|
|
7
|
+
symbol?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
image?: string;
|
|
11
|
+
uiAmount?: number;
|
|
12
|
+
price?: number;
|
|
13
|
+
value?: number;
|
|
14
|
+
supply?: number;
|
|
15
|
+
marketCap?: number;
|
|
16
|
+
lastTraded?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TradedTokenApi {
|
|
20
|
+
mint: string;
|
|
21
|
+
symbol?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
image?: string;
|
|
24
|
+
lastTraded?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WindowHoverInfo {
|
|
28
|
+
address: string;
|
|
29
|
+
tokenIndex: number;
|
|
30
|
+
screenX: number;
|
|
31
|
+
screenY: number;
|
|
32
|
+
mode: "building" | "token" | "swap";
|
|
33
|
+
// Identity fields (pre-loaded from wallets bulk response)
|
|
34
|
+
identityName?: string | null;
|
|
35
|
+
identityType?: string | null;
|
|
36
|
+
identityCategory?: string | null;
|
|
37
|
+
// Swap-specific fields (only when mode === "swap")
|
|
38
|
+
swapSignature?: string;
|
|
39
|
+
swapTokenIn?: string;
|
|
40
|
+
swapTokenOut?: string;
|
|
41
|
+
swapAmountSol?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tokenCache = new Map<string, TokenInfo[]>();
|
|
45
|
+
const pendingBalances = new Set<string>();
|
|
46
|
+
const pendingTraded = new Set<string>();
|
|
47
|
+
|
|
48
|
+
function formatNum(n: number): string {
|
|
49
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
|
|
50
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
51
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(2)}K`;
|
|
52
|
+
if (n >= 1) return n.toFixed(2);
|
|
53
|
+
if (n >= 0.001) return n.toFixed(4);
|
|
54
|
+
return n.toExponential(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatUsd(n: number): string {
|
|
58
|
+
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`;
|
|
59
|
+
if (n >= 1_000) return `$${(n / 1_000).toFixed(2)}K`;
|
|
60
|
+
if (n >= 0.01) return `$${n.toFixed(2)}`;
|
|
61
|
+
if (n > 0) return `$${n.toFixed(6)}`;
|
|
62
|
+
return "$0";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatTimeAgo(unixTimestamp: number): string {
|
|
66
|
+
const seconds = Math.floor(Date.now() / 1000 - unixTimestamp);
|
|
67
|
+
if (seconds < 60) return "just now";
|
|
68
|
+
const minutes = Math.floor(seconds / 60);
|
|
69
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
70
|
+
const hours = Math.floor(minutes / 60);
|
|
71
|
+
if (hours < 24) return `${hours}h ago`;
|
|
72
|
+
const days = Math.floor(hours / 24);
|
|
73
|
+
if (days < 30) return `${days}d ago`;
|
|
74
|
+
const months = Math.floor(days / 30);
|
|
75
|
+
if (months < 12) return `${months}mo ago`;
|
|
76
|
+
const years = Math.floor(days / 365);
|
|
77
|
+
return `${years}y ago`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Intermediate caches for merging
|
|
81
|
+
const balancesCache = new Map<string, TokenInfo[]>();
|
|
82
|
+
const tradedCache = new Map<string, TokenInfo[]>();
|
|
83
|
+
|
|
84
|
+
function mergeTokens(address: string): TokenInfo[] {
|
|
85
|
+
const balances = balancesCache.get(address) ?? [];
|
|
86
|
+
const traded = tradedCache.get(address) ?? [];
|
|
87
|
+
|
|
88
|
+
// Build a lookup of lastTraded timestamps from traded list
|
|
89
|
+
const tradedMap = new Map<string, TokenInfo>();
|
|
90
|
+
for (const t of traded) {
|
|
91
|
+
tradedMap.set(t.mint, t);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
const combined: TokenInfo[] = [];
|
|
96
|
+
|
|
97
|
+
// Current holdings first — merge in lastTraded if available
|
|
98
|
+
for (const t of balances) {
|
|
99
|
+
seen.add(t.mint);
|
|
100
|
+
const tradedInfo = tradedMap.get(t.mint);
|
|
101
|
+
combined.push(tradedInfo?.lastTraded != null ? { ...t, lastTraded: tradedInfo.lastTraded } : t);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Then historical traded tokens not currently held
|
|
105
|
+
for (const t of traded) {
|
|
106
|
+
if (!seen.has(t.mint)) {
|
|
107
|
+
seen.add(t.mint);
|
|
108
|
+
combined.push(t);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return combined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Building profile cache ---
|
|
116
|
+
interface BuildingProfile {
|
|
117
|
+
x_username: string | null;
|
|
118
|
+
x_avatar_url: string | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const profileCache = new Map<string, BuildingProfile | null>();
|
|
122
|
+
const pendingProfiles = new Set<string>();
|
|
123
|
+
|
|
124
|
+
function BuildingTooltip({
|
|
125
|
+
address,
|
|
126
|
+
screenX,
|
|
127
|
+
screenY,
|
|
128
|
+
identityName,
|
|
129
|
+
identityType,
|
|
130
|
+
identityCategory,
|
|
131
|
+
}: {
|
|
132
|
+
address: string;
|
|
133
|
+
screenX: number;
|
|
134
|
+
screenY: number;
|
|
135
|
+
identityName?: string | null;
|
|
136
|
+
identityType?: string | null;
|
|
137
|
+
identityCategory?: string | null;
|
|
138
|
+
}) {
|
|
139
|
+
const [profile, setProfile] = useState<BuildingProfile | null | undefined>(
|
|
140
|
+
profileCache.has(address) ? profileCache.get(address)! : undefined,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (profileCache.has(address)) {
|
|
145
|
+
setProfile(profileCache.get(address)!);
|
|
146
|
+
} else if (!pendingProfiles.has(address)) {
|
|
147
|
+
pendingProfiles.add(address);
|
|
148
|
+
import("@/lib/supabase").then(({ createClient }) => {
|
|
149
|
+
const supabase = createClient();
|
|
150
|
+
Promise.resolve(
|
|
151
|
+
supabase
|
|
152
|
+
.from("profiles")
|
|
153
|
+
.select("x_username, x_avatar_url")
|
|
154
|
+
.eq("wallet_address", address)
|
|
155
|
+
.single()
|
|
156
|
+
)
|
|
157
|
+
.then(({ data }) => {
|
|
158
|
+
profileCache.set(address, data ?? null);
|
|
159
|
+
pendingProfiles.delete(address);
|
|
160
|
+
setProfile(data ?? null);
|
|
161
|
+
})
|
|
162
|
+
.catch(() => {
|
|
163
|
+
profileCache.set(address, null);
|
|
164
|
+
pendingProfiles.delete(address);
|
|
165
|
+
setProfile(null);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}, [address]);
|
|
170
|
+
|
|
171
|
+
// Identity is pre-loaded from the wallets bulk response — no fetch needed
|
|
172
|
+
const identity = identityName ? { name: identityName, type: identityType ?? "unknown", category: identityCategory ?? "" } : null;
|
|
173
|
+
|
|
174
|
+
// Extract @handle from identity name (formats: "@handle", "Display Name @handle")
|
|
175
|
+
const identityAtMatch = identity?.name?.match(/@(\w+)/);
|
|
176
|
+
const identityXHandle = identityAtMatch ? identityAtMatch[1] : null;
|
|
177
|
+
const identityDisplayName = identity?.name ? identity.name.replace(/@\w+/, "").trim() || null : null;
|
|
178
|
+
|
|
179
|
+
const shortAddress = `${address.slice(0, 4)}...${address.slice(-4)}`;
|
|
180
|
+
const isVerified = profile !== undefined && profile !== null;
|
|
181
|
+
const hasX = (isVerified && !!profile.x_username) || !!identityXHandle;
|
|
182
|
+
const xUsername = profile?.x_username ?? identityXHandle;
|
|
183
|
+
const xAvatar = profile?.x_avatar_url ?? null;
|
|
184
|
+
|
|
185
|
+
const tooltipWidth = 220;
|
|
186
|
+
const tooltipHeight = 80;
|
|
187
|
+
let left = screenX + 16;
|
|
188
|
+
let top = screenY - 12;
|
|
189
|
+
if (typeof window !== "undefined") {
|
|
190
|
+
if (left + tooltipWidth > window.innerWidth - 8) left = screenX - tooltipWidth - 8;
|
|
191
|
+
if (top + tooltipHeight > window.innerHeight - 8) top = screenY - tooltipHeight;
|
|
192
|
+
if (top < 8) top = 8;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div
|
|
197
|
+
className="fixed z-50 bg-black/60 backdrop-blur-xl border border-white/[0.08] rounded-2xl p-3.5 pointer-events-none"
|
|
198
|
+
style={{ left, top, width: tooltipWidth }}
|
|
199
|
+
>
|
|
200
|
+
{/* Row 1: Display name */}
|
|
201
|
+
{profile === undefined && !identity ? (
|
|
202
|
+
<div className="text-xs text-white/30 mb-1">Loading...</div>
|
|
203
|
+
) : identityDisplayName ? (
|
|
204
|
+
<div className="flex items-center gap-2 mb-1">
|
|
205
|
+
{xAvatar && (
|
|
206
|
+
<img src={xAvatar} alt="" className="w-5 h-5 rounded-full shrink-0" />
|
|
207
|
+
)}
|
|
208
|
+
<span className="text-sm font-medium text-white/80 truncate">{identityDisplayName}</span>
|
|
209
|
+
</div>
|
|
210
|
+
) : identity && !identityXHandle ? (
|
|
211
|
+
<div className="flex items-center gap-2 mb-1">
|
|
212
|
+
<span className="text-sm font-medium text-white/80 truncate">{identity.name}</span>
|
|
213
|
+
<span className="px-1.5 py-0.5 bg-white/[0.06] border border-white/[0.08] rounded-lg text-xs text-white/35 shrink-0">
|
|
214
|
+
{identity.category || identity.type}
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
) : null}
|
|
218
|
+
|
|
219
|
+
{/* Row 2: X handle + verified badge */}
|
|
220
|
+
{hasX ? (
|
|
221
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
222
|
+
{!identityDisplayName && xAvatar && (
|
|
223
|
+
<img src={xAvatar} alt="" className="w-5 h-5 rounded-full shrink-0" />
|
|
224
|
+
)}
|
|
225
|
+
<a href={`https://x.com/${xUsername}`} target="_blank" rel="noopener noreferrer" className="text-sm text-blue-300 truncate hover:underline pointer-events-auto">@{xUsername}</a>
|
|
226
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 bg-green-500/10 border border-green-500/15 rounded-lg text-xs text-green-300 shrink-0">
|
|
227
|
+
<svg width="10" height="10" viewBox="0 0 20 20" fill="currentColor">
|
|
228
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
229
|
+
</svg>
|
|
230
|
+
Verified
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
) : isVerified ? (
|
|
234
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
235
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 bg-green-500/10 border border-green-500/15 rounded-lg text-xs text-green-300">
|
|
236
|
+
<svg width="10" height="10" viewBox="0 0 20 20" fill="currentColor">
|
|
237
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
238
|
+
</svg>
|
|
239
|
+
Verified
|
|
240
|
+
</span>
|
|
241
|
+
</div>
|
|
242
|
+
) : !identity ? (
|
|
243
|
+
<div className="text-xs text-white/30 mb-1.5">Unverified</div>
|
|
244
|
+
) : null}
|
|
245
|
+
|
|
246
|
+
{/* Row 3: Wallet address */}
|
|
247
|
+
<div className="font-mono text-xs text-purple-300/50">{shortAddress}</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- Token tooltip (existing logic) ---
|
|
253
|
+
function TokenTooltip({ address, tokenIndex, screenX, screenY }: { address: string; tokenIndex: number; screenX: number; screenY: number }) {
|
|
254
|
+
const [tokens, setTokens] = useState<TokenInfo[] | null>(() => tokenCache.get(address) ?? null);
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (tokenCache.has(address)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let balancesDone = balancesCache.has(address);
|
|
262
|
+
let tradedDone = tradedCache.has(address);
|
|
263
|
+
|
|
264
|
+
const tryMerge = () => {
|
|
265
|
+
if (balancesDone && tradedDone) {
|
|
266
|
+
const merged = mergeTokens(address);
|
|
267
|
+
tokenCache.set(address, merged);
|
|
268
|
+
setTokens(merged);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (!balancesDone && !pendingBalances.has(address)) {
|
|
273
|
+
pendingBalances.add(address);
|
|
274
|
+
fetch(`/api/wallet/${address}/balances`)
|
|
275
|
+
.then((r) => r.json())
|
|
276
|
+
.then((data) => {
|
|
277
|
+
balancesCache.set(address, data.tokens ?? []);
|
|
278
|
+
pendingBalances.delete(address);
|
|
279
|
+
balancesDone = true;
|
|
280
|
+
tryMerge();
|
|
281
|
+
})
|
|
282
|
+
.catch(() => {
|
|
283
|
+
pendingBalances.delete(address);
|
|
284
|
+
balancesCache.set(address, []);
|
|
285
|
+
balancesDone = true;
|
|
286
|
+
tryMerge();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!tradedDone && !pendingTraded.has(address)) {
|
|
291
|
+
pendingTraded.add(address);
|
|
292
|
+
fetch(`/api/wallet/${address}/traded-tokens`)
|
|
293
|
+
.then((r) => r.json())
|
|
294
|
+
.then((data) => {
|
|
295
|
+
const traded: TokenInfo[] = ((data.mints ?? []) as TradedTokenApi[]).map((m) => ({
|
|
296
|
+
mint: m.mint,
|
|
297
|
+
symbol: m.symbol,
|
|
298
|
+
name: m.name,
|
|
299
|
+
image: m.image,
|
|
300
|
+
lastTraded: m.lastTraded,
|
|
301
|
+
}));
|
|
302
|
+
tradedCache.set(address, traded);
|
|
303
|
+
pendingTraded.delete(address);
|
|
304
|
+
tradedDone = true;
|
|
305
|
+
tryMerge();
|
|
306
|
+
})
|
|
307
|
+
.catch(() => {
|
|
308
|
+
pendingTraded.delete(address);
|
|
309
|
+
tradedCache.set(address, []);
|
|
310
|
+
tradedDone = true;
|
|
311
|
+
tryMerge();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (balancesDone && tradedDone) {
|
|
316
|
+
tryMerge();
|
|
317
|
+
}
|
|
318
|
+
}, [address]);
|
|
319
|
+
|
|
320
|
+
if (!tokens || tokens.length === 0) return null;
|
|
321
|
+
|
|
322
|
+
const token = tokens[tokenIndex % tokens.length];
|
|
323
|
+
const label =
|
|
324
|
+
token.symbol ||
|
|
325
|
+
token.name ||
|
|
326
|
+
`${token.mint.slice(0, 4)}...${token.mint.slice(-4)}`;
|
|
327
|
+
|
|
328
|
+
const tooltipWidth = 240;
|
|
329
|
+
const tooltipHeight = 180;
|
|
330
|
+
let left = screenX + 16;
|
|
331
|
+
let top = screenY - 12;
|
|
332
|
+
if (typeof window !== "undefined") {
|
|
333
|
+
if (left + tooltipWidth > window.innerWidth - 8) left = screenX - tooltipWidth - 8;
|
|
334
|
+
if (top + tooltipHeight > window.innerHeight - 8) top = screenY - tooltipHeight;
|
|
335
|
+
if (top < 8) top = 8;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
className="fixed z-50 bg-black/60 backdrop-blur-xl border border-white/[0.08] rounded-2xl p-3.5 pointer-events-none"
|
|
341
|
+
style={{ left, top, width: tooltipWidth }}
|
|
342
|
+
>
|
|
343
|
+
<div className="flex items-center gap-2.5 mb-2.5">
|
|
344
|
+
{token.image && (
|
|
345
|
+
<img src={token.image} alt="" className="w-6 h-6 rounded-full shrink-0" />
|
|
346
|
+
)}
|
|
347
|
+
<div className="min-w-0">
|
|
348
|
+
<div className="text-sm font-medium text-white truncate">{label}</div>
|
|
349
|
+
{token.name && token.symbol && token.name !== token.symbol && (
|
|
350
|
+
<div className="text-xs text-white/35 truncate">{token.name}</div>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div className="space-y-1.5 text-xs">
|
|
356
|
+
{token.uiAmount != null && (
|
|
357
|
+
<div className="flex justify-between">
|
|
358
|
+
<span className="text-white/35">Balance</span>
|
|
359
|
+
<span className="text-white/70 font-mono">{formatNum(token.uiAmount)}</span>
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
{token.price != null && (
|
|
363
|
+
<div className="flex justify-between">
|
|
364
|
+
<span className="text-white/35">Price</span>
|
|
365
|
+
<span className="text-white/70 font-mono">{formatUsd(token.price)}</span>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
{token.value != null && (
|
|
369
|
+
<div className="flex justify-between">
|
|
370
|
+
<span className="text-white/35">Value</span>
|
|
371
|
+
<span className="text-white/70 font-mono">{formatUsd(token.value)}</span>
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
{token.supply != null && (
|
|
375
|
+
<div className="flex justify-between">
|
|
376
|
+
<span className="text-white/35">Supply</span>
|
|
377
|
+
<span className="text-white/70 font-mono">{formatNum(token.supply)}</span>
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
{token.marketCap != null && token.marketCap > 0 && (
|
|
381
|
+
<div className="flex justify-between">
|
|
382
|
+
<span className="text-white/35">Market Cap</span>
|
|
383
|
+
<span className="text-white/70 font-mono">{formatUsd(token.marketCap)}</span>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
{token.lastTraded != null && (
|
|
387
|
+
<div className="flex justify-between">
|
|
388
|
+
<span className="text-white/35">Last traded</span>
|
|
389
|
+
<span className="text-white/70 font-mono">{formatTimeAgo(token.lastTraded)}</span>
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div className="mt-2.5 pt-2 border-t border-white/[0.06] text-xs text-white/20 font-mono truncate">
|
|
395
|
+
{token.mint}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// --- Swap tooltip ---
|
|
402
|
+
function SwapTooltip({
|
|
403
|
+
address,
|
|
404
|
+
screenX,
|
|
405
|
+
screenY,
|
|
406
|
+
signature,
|
|
407
|
+
tokenIn,
|
|
408
|
+
tokenOut,
|
|
409
|
+
amountSol,
|
|
410
|
+
}: {
|
|
411
|
+
address: string;
|
|
412
|
+
screenX: number;
|
|
413
|
+
screenY: number;
|
|
414
|
+
signature?: string;
|
|
415
|
+
tokenIn?: string;
|
|
416
|
+
tokenOut?: string;
|
|
417
|
+
amountSol?: number;
|
|
418
|
+
}) {
|
|
419
|
+
const shortAddress = `${address.slice(0, 4)}...${address.slice(-4)}`;
|
|
420
|
+
const shortMint = (mint?: string) =>
|
|
421
|
+
mint ? `${mint.slice(0, 4)}...${mint.slice(-4)}` : "???";
|
|
422
|
+
|
|
423
|
+
const tooltipWidth = 240;
|
|
424
|
+
const tooltipHeight = 130;
|
|
425
|
+
let left = screenX + 16;
|
|
426
|
+
let top = screenY - 12;
|
|
427
|
+
if (typeof window !== "undefined") {
|
|
428
|
+
if (left + tooltipWidth > window.innerWidth - 8) left = screenX - tooltipWidth - 8;
|
|
429
|
+
if (top + tooltipHeight > window.innerHeight - 8) top = screenY - tooltipHeight;
|
|
430
|
+
if (top < 8) top = 8;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return (
|
|
434
|
+
<div
|
|
435
|
+
className="fixed z-50 bg-black/60 backdrop-blur-xl border border-white/[0.08] rounded-2xl p-3.5"
|
|
436
|
+
style={{ left, top, width: tooltipWidth, pointerEvents: "auto", cursor: "default" }}
|
|
437
|
+
>
|
|
438
|
+
<div className="flex items-center gap-2 mb-2">
|
|
439
|
+
<span className="text-xs text-orange-300/80 font-medium">SWAP</span>
|
|
440
|
+
<span className="font-mono text-xs text-purple-300/50">{shortAddress}</span>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<div className="flex items-center gap-1.5 text-sm text-white/70 mb-2">
|
|
444
|
+
<span className="font-mono text-xs">{shortMint(tokenIn)}</span>
|
|
445
|
+
<span className="text-white/30">→</span>
|
|
446
|
+
<span className="font-mono text-xs">{shortMint(tokenOut)}</span>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{amountSol != null && (
|
|
450
|
+
<div className="flex justify-between text-xs mb-2">
|
|
451
|
+
<span className="text-white/35">Amount</span>
|
|
452
|
+
<span className="text-white/70 font-mono">{formatNum(amountSol)} SOL</span>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{signature && (
|
|
457
|
+
<a
|
|
458
|
+
href={`https://orbmarkets.io/tx/${signature}`}
|
|
459
|
+
target="_blank"
|
|
460
|
+
rel="noopener noreferrer"
|
|
461
|
+
className="block text-xs text-blue-400/60 hover:text-blue-400 transition-colors truncate cursor-pointer"
|
|
462
|
+
onClick={(e) => e.stopPropagation()}
|
|
463
|
+
>
|
|
464
|
+
View on Orb →
|
|
465
|
+
</a>
|
|
466
|
+
)}
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export default function WindowTooltip(props: WindowHoverInfo) {
|
|
472
|
+
if (props.mode === "building") {
|
|
473
|
+
return (
|
|
474
|
+
<BuildingTooltip
|
|
475
|
+
address={props.address}
|
|
476
|
+
screenX={props.screenX}
|
|
477
|
+
screenY={props.screenY}
|
|
478
|
+
identityName={props.identityName}
|
|
479
|
+
identityType={props.identityType}
|
|
480
|
+
identityCategory={props.identityCategory}
|
|
481
|
+
/>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (props.mode === "swap") {
|
|
485
|
+
return (
|
|
486
|
+
<SwapTooltip
|
|
487
|
+
address={props.address}
|
|
488
|
+
screenX={props.screenX}
|
|
489
|
+
screenY={props.screenY}
|
|
490
|
+
signature={props.swapSignature}
|
|
491
|
+
tokenIn={props.swapTokenIn}
|
|
492
|
+
tokenOut={props.swapTokenOut}
|
|
493
|
+
amountSol={props.swapAmountSol}
|
|
494
|
+
/>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return <TokenTooltip address={props.address} tokenIndex={props.tokenIndex} screenX={props.screenX} screenY={props.screenY} />;
|
|
498
|
+
}
|