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,83 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Treasury balance API — fetches SOL balance of the reward treasury wallet.
|
|
5
|
+
* Primary: Helius RPC getBalance (reliable, fast)
|
|
6
|
+
* Also fetches SOL price from Helius Wallet API for USD display
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TREASURY_ADDRESS = "76QQqndM87DBQAeAnRnjdMSiVBgBNrm55HMa88MRVc7p";
|
|
10
|
+
const REWARD_PER_PARCEL = 0.069420;
|
|
11
|
+
|
|
12
|
+
export async function GET() {
|
|
13
|
+
try {
|
|
14
|
+
const apiKey = process.env.HELIUS_API_KEY;
|
|
15
|
+
const rpcUrl = process.env.HELIUS_RPC_URL || `https://mainnet.helius-rpc.com/?api-key=${apiKey}`;
|
|
16
|
+
|
|
17
|
+
// Primary: getBalance via RPC (most reliable)
|
|
18
|
+
const balRes = await fetch(rpcUrl, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
id: "treasury-bal",
|
|
24
|
+
method: "getBalance",
|
|
25
|
+
params: [TREASURY_ADDRESS],
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const balData = await balRes.json();
|
|
30
|
+
const lamports = balData?.result?.value ?? 0;
|
|
31
|
+
const sol = lamports / 1_000_000_000;
|
|
32
|
+
|
|
33
|
+
// Try to get SOL price from Helius Wallet API for USD display
|
|
34
|
+
let solPrice: number | null = null;
|
|
35
|
+
let solUsdValue: number | null = null;
|
|
36
|
+
|
|
37
|
+
if (apiKey) {
|
|
38
|
+
try {
|
|
39
|
+
const walletRes = await fetch(
|
|
40
|
+
`https://api.helius.xyz/v1/wallet/${TREASURY_ADDRESS}/balances?api-key=${apiKey}&showNative=true`,
|
|
41
|
+
{ signal: AbortSignal.timeout(5000) },
|
|
42
|
+
);
|
|
43
|
+
if (walletRes.ok) {
|
|
44
|
+
const wData = await walletRes.json();
|
|
45
|
+
const solEntry = wData.balances?.find(
|
|
46
|
+
(b: { mint: string }) => b.mint === "So11111111111111111111111111111111111111112",
|
|
47
|
+
);
|
|
48
|
+
if (solEntry) {
|
|
49
|
+
solPrice = solEntry.pricePerToken ?? null;
|
|
50
|
+
solUsdValue = solEntry.usdValue ?? null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Wallet API failed — still have balance from RPC, just no USD price
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Calculate how many 0.069420 SOL parcels can still be funded
|
|
59
|
+
const TX_FEE = 0.000005;
|
|
60
|
+
const parcelsRemaining = Math.floor(sol / (REWARD_PER_PARCEL + TX_FEE));
|
|
61
|
+
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
address: TREASURY_ADDRESS,
|
|
64
|
+
balanceSOL: Math.round(sol * 1_000_000) / 1_000_000,
|
|
65
|
+
balanceLamports: lamports,
|
|
66
|
+
solUsdValue,
|
|
67
|
+
solPrice,
|
|
68
|
+
parcelsRemaining,
|
|
69
|
+
rewardPerParcel: REWARD_PER_PARCEL,
|
|
70
|
+
source: "helius-rpc",
|
|
71
|
+
}, {
|
|
72
|
+
headers: { "Cache-Control": "public, max-age=15" },
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error("Treasury balance error:", err);
|
|
76
|
+
return NextResponse.json({
|
|
77
|
+
address: TREASURY_ADDRESS,
|
|
78
|
+
balanceSOL: 0,
|
|
79
|
+
parcelsRemaining: 0,
|
|
80
|
+
error: "Failed to fetch balance",
|
|
81
|
+
}, { status: 500 });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const HELIUS_API_KEY = process.env.HELIUS_API_KEY!;
|
|
4
|
+
const HELIUS_RPC_URL = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`;
|
|
5
|
+
|
|
6
|
+
interface TokenInfo {
|
|
7
|
+
mint: string;
|
|
8
|
+
symbol?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
image?: string;
|
|
12
|
+
amount: number;
|
|
13
|
+
decimals: number;
|
|
14
|
+
uiAmount: number;
|
|
15
|
+
price?: number;
|
|
16
|
+
value?: number;
|
|
17
|
+
supply?: number;
|
|
18
|
+
marketCap?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function GET(
|
|
22
|
+
_req: NextRequest,
|
|
23
|
+
{ params }: { params: Promise<{ address: string }> },
|
|
24
|
+
) {
|
|
25
|
+
const { address } = await params;
|
|
26
|
+
|
|
27
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ error: "Invalid Solana address" },
|
|
30
|
+
{ status: 400 },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Single DAS call to get all fungible tokens with metadata, prices, and native balance
|
|
36
|
+
const dasRes = await fetch(HELIUS_RPC_URL, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
jsonrpc: "2.0",
|
|
41
|
+
id: 1,
|
|
42
|
+
method: "getAssetsByOwner",
|
|
43
|
+
params: {
|
|
44
|
+
ownerAddress: address,
|
|
45
|
+
page: 1,
|
|
46
|
+
limit: 100,
|
|
47
|
+
displayOptions: {
|
|
48
|
+
showFungible: true,
|
|
49
|
+
showNativeBalance: true,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const dasJson = await dasRes.json();
|
|
56
|
+
if (dasJson.error) {
|
|
57
|
+
throw new Error(`DAS error: ${dasJson.error.message}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = dasJson.result;
|
|
61
|
+
const solBalance = (result.nativeBalance?.lamports ?? 0) / 1e9;
|
|
62
|
+
|
|
63
|
+
// Filter to fungible tokens with balance > 0
|
|
64
|
+
const tokens: TokenInfo[] = [];
|
|
65
|
+
for (const item of result.items ?? []) {
|
|
66
|
+
const isFungible =
|
|
67
|
+
item.interface === "FungibleToken" || item.interface === "FungibleAsset";
|
|
68
|
+
if (!isFungible) continue;
|
|
69
|
+
|
|
70
|
+
const ti = item.token_info;
|
|
71
|
+
if (!ti || !ti.balance || ti.balance <= 0) continue;
|
|
72
|
+
|
|
73
|
+
const decimals = ti.decimals ?? 0;
|
|
74
|
+
const uiAmount = ti.balance / Math.pow(10, decimals);
|
|
75
|
+
const price = ti.price_info?.price_per_token ?? undefined;
|
|
76
|
+
const supply = ti.supply != null
|
|
77
|
+
? ti.supply / Math.pow(10, decimals)
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
80
|
+
tokens.push({
|
|
81
|
+
mint: item.id,
|
|
82
|
+
symbol:
|
|
83
|
+
item.content?.metadata?.symbol ||
|
|
84
|
+
ti.symbol ||
|
|
85
|
+
undefined,
|
|
86
|
+
name: item.content?.metadata?.name || undefined,
|
|
87
|
+
description: item.content?.metadata?.description || undefined,
|
|
88
|
+
image:
|
|
89
|
+
item.content?.links?.image ||
|
|
90
|
+
item.content?.files?.[0]?.cdn_uri ||
|
|
91
|
+
item.content?.files?.[0]?.uri ||
|
|
92
|
+
undefined,
|
|
93
|
+
amount: ti.balance,
|
|
94
|
+
decimals,
|
|
95
|
+
uiAmount,
|
|
96
|
+
price,
|
|
97
|
+
value: price != null ? uiAmount * price : undefined,
|
|
98
|
+
supply,
|
|
99
|
+
marketCap:
|
|
100
|
+
price != null && supply != null ? supply * price : undefined,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort by value (USD) descending, fall back to uiAmount
|
|
105
|
+
tokens.sort((a, b) => (b.value ?? 0) - (a.value ?? 0) || b.uiAmount - a.uiAmount);
|
|
106
|
+
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{
|
|
109
|
+
solBalance,
|
|
110
|
+
tokenCount: tokens.length,
|
|
111
|
+
tokens: tokens.slice(0, 20),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
headers: { "Cache-Control": "public, max-age=60" },
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("Balance fetch error:", err);
|
|
119
|
+
return NextResponse.json(
|
|
120
|
+
{ error: err instanceof Error ? err.message : "Unknown error" },
|
|
121
|
+
{ status: 500 },
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createAdminClient } from "@/lib/supabase-admin";
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_req: Request,
|
|
6
|
+
{ params }: { params: Promise<{ address: string }> },
|
|
7
|
+
) {
|
|
8
|
+
const { address } = await params;
|
|
9
|
+
const supabase = createAdminClient();
|
|
10
|
+
|
|
11
|
+
// Read-only — return cached identity, no writes
|
|
12
|
+
const { data: row } = await supabase
|
|
13
|
+
.from("wallets")
|
|
14
|
+
.select("identity_name, identity_type, identity_category")
|
|
15
|
+
.eq("address", address)
|
|
16
|
+
.single();
|
|
17
|
+
|
|
18
|
+
if (row?.identity_name) {
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
identity: {
|
|
21
|
+
name: row.identity_name,
|
|
22
|
+
type: row.identity_type ?? "unknown",
|
|
23
|
+
category: row.identity_category ?? "",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return NextResponse.json(
|
|
29
|
+
{ identity: null },
|
|
30
|
+
{ headers: { "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=86400" } },
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { WalletStats, getWalletIdentity, getWalletFunding } from "@/lib/helius";
|
|
3
|
+
import { createAdminClient } from "@/lib/supabase-admin";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GET /api/wallet/[address]
|
|
7
|
+
*
|
|
8
|
+
* If the wallet exists in the DB, return its stats.
|
|
9
|
+
* If NOT, auto-ingest it using Helius APIs (Enhanced Transactions + Identity),
|
|
10
|
+
* place it in the city grid, and return the result.
|
|
11
|
+
*/
|
|
12
|
+
export async function GET(
|
|
13
|
+
_req: NextRequest,
|
|
14
|
+
{ params }: { params: Promise<{ address: string }> },
|
|
15
|
+
) {
|
|
16
|
+
const { address } = await params;
|
|
17
|
+
|
|
18
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: "Invalid Solana address" },
|
|
21
|
+
{ status: 400 },
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const supabase = createAdminClient();
|
|
27
|
+
|
|
28
|
+
// Check if wallet already exists
|
|
29
|
+
const { data: existing, error: selectError } = await supabase
|
|
30
|
+
.from("wallets")
|
|
31
|
+
.select(
|
|
32
|
+
"address, txn_count, volume_traded_sol, fees_paid_sol, wallet_age_days, first_tx_at, ingestion_status, unique_tokens_swapped, latest_tx_at",
|
|
33
|
+
)
|
|
34
|
+
.eq("address", address)
|
|
35
|
+
.single();
|
|
36
|
+
|
|
37
|
+
if (selectError && selectError.code !== "PGRST116") {
|
|
38
|
+
throw new Error(`DB error: ${selectError.message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Wallet already in the city — return its stats
|
|
42
|
+
if (existing) {
|
|
43
|
+
const stats: WalletStats = {
|
|
44
|
+
address: existing.address,
|
|
45
|
+
txnCount: existing.txn_count,
|
|
46
|
+
walletAgeDays: existing.wallet_age_days,
|
|
47
|
+
volumeTraded: existing.volume_traded_sol,
|
|
48
|
+
feesPaid: existing.fees_paid_sol,
|
|
49
|
+
firstTxTimestamp: existing.first_tx_at
|
|
50
|
+
? Math.floor(new Date(existing.first_tx_at).getTime() / 1000)
|
|
51
|
+
: null,
|
|
52
|
+
ingestionStatus: existing.ingestion_status,
|
|
53
|
+
uniqueTokensSwapped: existing.unique_tokens_swapped,
|
|
54
|
+
latestBlocktime: existing.latest_tx_at
|
|
55
|
+
? Math.floor(new Date(existing.latest_tx_at).getTime() / 1000)
|
|
56
|
+
: null,
|
|
57
|
+
};
|
|
58
|
+
return NextResponse.json(stats);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- NEW WALLET: Auto-ingest using Helius APIs ---
|
|
62
|
+
|
|
63
|
+
// Check 1,000 wallet cap
|
|
64
|
+
const { count: walletCount } = await supabase
|
|
65
|
+
.from("wallets")
|
|
66
|
+
.select("*", { count: "exact", head: true });
|
|
67
|
+
|
|
68
|
+
if ((walletCount ?? 0) >= 1000) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: "City is full! Only 1,000 spots available." },
|
|
71
|
+
{ status: 429 },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rpcUrl = process.env.HELIUS_RPC_URL;
|
|
76
|
+
if (!rpcUrl) {
|
|
77
|
+
return NextResponse.json(
|
|
78
|
+
{ error: "RPC not configured" },
|
|
79
|
+
{ status: 500 },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fetch transaction signatures to get real stats
|
|
84
|
+
const [sigRes, identityResult, fundingResult] = await Promise.all([
|
|
85
|
+
fetch(rpcUrl, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "Content-Type": "application/json" },
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
jsonrpc: "2.0",
|
|
90
|
+
id: "sigs",
|
|
91
|
+
method: "getSignaturesForAddress",
|
|
92
|
+
params: [address, { limit: 1000 }],
|
|
93
|
+
}),
|
|
94
|
+
}),
|
|
95
|
+
getWalletIdentity(address).catch(() => null),
|
|
96
|
+
getWalletFunding(address).catch(() => null),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const sigData = await sigRes.json();
|
|
100
|
+
const sigs = sigData.result ?? [];
|
|
101
|
+
|
|
102
|
+
if (sigs.length === 0) {
|
|
103
|
+
return NextResponse.json(
|
|
104
|
+
{ error: "No transactions found for this wallet" },
|
|
105
|
+
{ status: 404 },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Calculate basic stats from signatures
|
|
110
|
+
const txnCount = sigs.length;
|
|
111
|
+
const now = Date.now() / 1000;
|
|
112
|
+
|
|
113
|
+
// Oldest and newest blocktimes
|
|
114
|
+
const blocktimes = sigs
|
|
115
|
+
.map((s: { blockTime: number | null }) => s.blockTime)
|
|
116
|
+
.filter((bt: number | null): bt is number => bt !== null);
|
|
117
|
+
|
|
118
|
+
const oldestBlocktime = blocktimes.length > 0 ? Math.min(...blocktimes) : null;
|
|
119
|
+
const newestBlocktime = blocktimes.length > 0 ? Math.max(...blocktimes) : null;
|
|
120
|
+
|
|
121
|
+
// Wallet age from funding info or oldest tx
|
|
122
|
+
let firstTxAt: Date | null = null;
|
|
123
|
+
let walletAgeDays = 0;
|
|
124
|
+
|
|
125
|
+
if (fundingResult?.timestamp) {
|
|
126
|
+
firstTxAt = new Date(fundingResult.timestamp * 1000);
|
|
127
|
+
walletAgeDays = Math.floor((now - fundingResult.timestamp) / 86400);
|
|
128
|
+
} else if (oldestBlocktime) {
|
|
129
|
+
firstTxAt = new Date(oldestBlocktime * 1000);
|
|
130
|
+
walletAgeDays = Math.floor((now - oldestBlocktime) / 86400);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const latestTxAt = newestBlocktime ? new Date(newestBlocktime * 1000) : null;
|
|
134
|
+
|
|
135
|
+
// Estimate volume from SOL balance check
|
|
136
|
+
const balRes = await fetch(rpcUrl, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "Content-Type": "application/json" },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
jsonrpc: "2.0",
|
|
141
|
+
id: "bal",
|
|
142
|
+
method: "getBalance",
|
|
143
|
+
params: [address],
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
const balData = await balRes.json();
|
|
147
|
+
const solBalance = (balData.result?.value ?? 0) / 1e9;
|
|
148
|
+
|
|
149
|
+
// Rough volume estimate based on tx count and balance
|
|
150
|
+
const volumeEstimate = solBalance + txnCount * 0.05;
|
|
151
|
+
const feesEstimate = txnCount * 0.000005;
|
|
152
|
+
|
|
153
|
+
// Get token accounts count as unique tokens estimate
|
|
154
|
+
const tokenRes = await fetch(rpcUrl, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
jsonrpc: "2.0",
|
|
159
|
+
id: "tokens",
|
|
160
|
+
method: "getTokenAccountsByOwner",
|
|
161
|
+
params: [
|
|
162
|
+
address,
|
|
163
|
+
{ programId: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" },
|
|
164
|
+
{ encoding: "jsonParsed" },
|
|
165
|
+
],
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
const tokenData = await tokenRes.json();
|
|
169
|
+
const uniqueTokens = tokenData.result?.value?.length ?? 0;
|
|
170
|
+
|
|
171
|
+
// Insert the wallet
|
|
172
|
+
const { error: insertError } = await supabase.from("wallets").upsert({
|
|
173
|
+
address,
|
|
174
|
+
txn_count: txnCount,
|
|
175
|
+
volume_traded_sol: Number(volumeEstimate.toFixed(4)),
|
|
176
|
+
fees_paid_sol: Number(feesEstimate.toFixed(6)),
|
|
177
|
+
wallet_age_days: walletAgeDays,
|
|
178
|
+
first_tx_at: firstTxAt?.toISOString() ?? null,
|
|
179
|
+
latest_tx_at: latestTxAt?.toISOString() ?? null,
|
|
180
|
+
ingestion_status: "complete",
|
|
181
|
+
unique_tokens_swapped: uniqueTokens,
|
|
182
|
+
identity_name: identityResult?.name ?? null,
|
|
183
|
+
identity_type: identityResult?.type ?? null,
|
|
184
|
+
identity_category: identityResult?.category ?? null,
|
|
185
|
+
updated_at: new Date().toISOString(),
|
|
186
|
+
}, { onConflict: "address" });
|
|
187
|
+
|
|
188
|
+
if (insertError) {
|
|
189
|
+
throw new Error(`Insert error: ${insertError.message}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Assign a city position
|
|
193
|
+
await supabase.rpc("assign_city_position", { p_address: address });
|
|
194
|
+
|
|
195
|
+
// Return the new wallet stats
|
|
196
|
+
const stats: WalletStats = {
|
|
197
|
+
address,
|
|
198
|
+
txnCount,
|
|
199
|
+
walletAgeDays,
|
|
200
|
+
volumeTraded: Number(volumeEstimate.toFixed(4)),
|
|
201
|
+
feesPaid: Number(feesEstimate.toFixed(6)),
|
|
202
|
+
firstTxTimestamp: oldestBlocktime ?? (fundingResult?.timestamp ?? null),
|
|
203
|
+
ingestionStatus: "complete",
|
|
204
|
+
uniqueTokensSwapped: uniqueTokens,
|
|
205
|
+
latestBlocktime: newestBlocktime,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return NextResponse.json({ ...stats, isNew: true });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error("Wallet fetch error:", err);
|
|
211
|
+
return NextResponse.json(
|
|
212
|
+
{ error: err instanceof Error ? err.message : "Unknown error" },
|
|
213
|
+
{ status: 500 },
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { createAdminClient } from "@/lib/supabase-admin";
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
_req: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ address: string }> },
|
|
7
|
+
) {
|
|
8
|
+
const { address } = await params;
|
|
9
|
+
|
|
10
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: "Invalid Solana address" },
|
|
13
|
+
{ status: 400 },
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const supabase = createAdminClient();
|
|
19
|
+
|
|
20
|
+
const { data: wallet, error } = await supabase
|
|
21
|
+
.from("wallets")
|
|
22
|
+
.select("traded_token_mints")
|
|
23
|
+
.eq("address", address)
|
|
24
|
+
.single();
|
|
25
|
+
|
|
26
|
+
if (error && error.code !== "PGRST116") {
|
|
27
|
+
throw new Error(`DB error: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ mints: wallet?.traded_token_mints ?? [] },
|
|
32
|
+
{ headers: { "Cache-Control": "public, max-age=300" } },
|
|
33
|
+
);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error("Traded tokens fetch error:", err);
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: err instanceof Error ? err.message : "Unknown error" },
|
|
38
|
+
{ status: 500 },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createAdminClient } from "@/lib/supabase-admin";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const supabase = createAdminClient();
|
|
7
|
+
|
|
8
|
+
// Self-heal: assign positions to any unplaced complete wallets
|
|
9
|
+
await supabase.rpc("repair_unplaced_wallets");
|
|
10
|
+
|
|
11
|
+
const [walletsResult, profilesResult] = await Promise.all([
|
|
12
|
+
supabase
|
|
13
|
+
.from("wallets")
|
|
14
|
+
.select(
|
|
15
|
+
"address, txn_count, volume_traded_sol, fees_paid_sol, wallet_age_days, block_row, block_col, local_slot, unique_tokens_swapped, latest_tx_at, identity_name, identity_type, identity_category"
|
|
16
|
+
)
|
|
17
|
+
.not("block_row", "is", null)
|
|
18
|
+
.limit(11000),
|
|
19
|
+
supabase
|
|
20
|
+
.from("profiles")
|
|
21
|
+
.select("wallet_address, x_username")
|
|
22
|
+
.not("x_username", "is", null),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
if (walletsResult.error) {
|
|
26
|
+
throw new Error(`DB error: ${walletsResult.error.message}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const profileMap = new Map<string, string>();
|
|
30
|
+
for (const p of profilesResult.data ?? []) {
|
|
31
|
+
if (p.wallet_address && p.x_username) {
|
|
32
|
+
profileMap.set(p.wallet_address, p.x_username);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const wallets = (walletsResult.data ?? []).map((row) => ({
|
|
37
|
+
address: row.address,
|
|
38
|
+
txnCount: row.txn_count,
|
|
39
|
+
walletAgeDays: row.wallet_age_days,
|
|
40
|
+
volumeTraded: row.volume_traded_sol,
|
|
41
|
+
feesPaid: row.fees_paid_sol,
|
|
42
|
+
blockRow: row.block_row,
|
|
43
|
+
blockCol: row.block_col,
|
|
44
|
+
localSlot: row.local_slot,
|
|
45
|
+
uniqueTokensSwapped: row.unique_tokens_swapped,
|
|
46
|
+
latestBlocktime: row.latest_tx_at ? Math.floor(new Date(row.latest_tx_at).getTime() / 1000) : null,
|
|
47
|
+
identityName: row.identity_name ?? null,
|
|
48
|
+
identityType: row.identity_type ?? null,
|
|
49
|
+
identityCategory: row.identity_category ?? null,
|
|
50
|
+
xUsername: profileMap.get(row.address) ?? null,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ wallets, totalPlaced: wallets.length },
|
|
55
|
+
{
|
|
56
|
+
headers: {
|
|
57
|
+
"Cache-Control": "public, max-age=30",
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error("Wallets fetch error:", err);
|
|
63
|
+
return NextResponse.json(
|
|
64
|
+
{ error: err instanceof Error ? err.message : "Unknown error" },
|
|
65
|
+
{ status: 500 }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { createAdminClient } from "@/lib/supabase-admin";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helius Enhanced Webhook handler.
|
|
6
|
+
* Receives SWAP events, stores them, and broadcasts to Supabase Realtime.
|
|
7
|
+
*/
|
|
8
|
+
export async function POST(req: NextRequest) {
|
|
9
|
+
// Verify auth header
|
|
10
|
+
const authHeader = req.headers.get("authorization");
|
|
11
|
+
const expected = `Bearer ${process.env.HELIUS_WEBHOOK_SECRET}`;
|
|
12
|
+
if (authHeader !== expected) {
|
|
13
|
+
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const events = await req.json();
|
|
18
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
19
|
+
return NextResponse.json({ received: 0 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const supabase = createAdminClient();
|
|
23
|
+
let stored = 0;
|
|
24
|
+
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
// Only process SWAP type
|
|
27
|
+
if (event.type !== "SWAP") continue;
|
|
28
|
+
|
|
29
|
+
const swap = event.events?.swap;
|
|
30
|
+
if (!swap) continue;
|
|
31
|
+
|
|
32
|
+
const feePayer = event.feePayer;
|
|
33
|
+
const signature = event.signature;
|
|
34
|
+
const timestamp = event.timestamp
|
|
35
|
+
? new Date(event.timestamp * 1000).toISOString()
|
|
36
|
+
: new Date().toISOString();
|
|
37
|
+
|
|
38
|
+
// Extract token in/out from swap event
|
|
39
|
+
const tokenIn =
|
|
40
|
+
swap.tokenInputs?.[0]?.mint ??
|
|
41
|
+
(swap.nativeInput?.account ? "So11111111111111111111111111111111" : null);
|
|
42
|
+
const tokenOut =
|
|
43
|
+
swap.tokenOutputs?.[0]?.mint ??
|
|
44
|
+
(swap.nativeOutput?.account ? "So11111111111111111111111111111111" : null);
|
|
45
|
+
|
|
46
|
+
// Compute SOL amount
|
|
47
|
+
let amountSol: number | null = null;
|
|
48
|
+
if (swap.nativeInput?.amount) {
|
|
49
|
+
amountSol = Number(swap.nativeInput.amount) / 1e9;
|
|
50
|
+
} else if (swap.nativeOutput?.amount) {
|
|
51
|
+
amountSol = Number(swap.nativeOutput.amount) / 1e9;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
// Insert swap event
|
|
56
|
+
const { error } = await supabase.from("swap_events").insert({
|
|
57
|
+
wallet_address: feePayer,
|
|
58
|
+
signature,
|
|
59
|
+
token_in: tokenIn,
|
|
60
|
+
token_out: tokenOut,
|
|
61
|
+
amount_sol: amountSol,
|
|
62
|
+
created_at: timestamp,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!error) stored++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return NextResponse.json({ received: events.length, stored });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("Webhook error:", err);
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{ error: String(err) },
|
|
73
|
+
{ status: 500 }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { usePhantom } from "@phantom/react-sdk";
|
|
5
|
+
|
|
6
|
+
export default function AuthCallback() {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
const { isConnected } = usePhantom();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (isConnected) {
|
|
12
|
+
router.replace("/");
|
|
13
|
+
}
|
|
14
|
+
}, [isConnected, router]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const timer = setTimeout(() => router.replace("/"), 3000);
|
|
18
|
+
return () => clearTimeout(timer);
|
|
19
|
+
}, [router]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="min-h-screen bg-[#0a0a12] flex items-center justify-center">
|
|
23
|
+
<div className="text-center">
|
|
24
|
+
<div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin mx-auto mb-4" />
|
|
25
|
+
<p className="text-white/60 text-sm">Connecting wallet...</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
Binary file
|