From 7db991b67b10f0a9669c12b109c90d283595c623 Mon Sep 17 00:00:00 2001 From: jmanhype Date: Mon, 26 May 2025 21:00:53 -0500 Subject: [PATCH 1/5] fix: add missing API functions for token analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make tokenId optional in bondingCurves schema for backward compatibility - Add getBondingCurve alias for getCurveData in bondingCurveApi - Add missing analytics queries: getTradeHistory, getHolderDistribution, getSocialMetrics - Fix schema validation error for existing bondingCurve documents 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- convex/analytics.ts | 114 ++++++++++++++++++++++++++++++++++++++ convex/bondingCurveApi.ts | 5 +- convex/schema.ts | 2 +- 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/convex/analytics.ts b/convex/analytics.ts index af9236c..d965672 100644 --- a/convex/analytics.ts +++ b/convex/analytics.ts @@ -326,6 +326,120 @@ export const updateTokenAnalytics = internalMutation({ }); // Fetch real-time analytics data from blockchain +// Get trade history for a token +export const getTradeHistory = query({ + args: { + tokenId: v.id("memeCoins"), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const limit = args.limit ?? 50; + + // Get bonding curve transactions + const bondingCurve = await ctx.db + .query("bondingCurves") + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) + .first(); + + if (!bondingCurve) { + return []; + } + + const transactions = await ctx.db + .query("bondingCurveTransactions") + .withIndex("by_curve", (q) => q.eq("bondingCurveId", bondingCurve._id)) + .order("desc") + .take(limit); + + return transactions.map((tx) => ({ + type: tx.type, + price: tx.price, + amount: tx.type === "buy" ? tx.tokensOut : tx.tokensIn, + value: tx.type === "buy" ? tx.amountIn : tx.amountOut, + timestamp: tx.timestamp, + user: tx.user, + })); + }, +}); + +// Get holder distribution +export const getHolderDistribution = query({ + args: { + tokenId: v.id("memeCoins"), + }, + handler: async (ctx, args) => { + const bondingCurve = await ctx.db + .query("bondingCurves") + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) + .first(); + + if (!bondingCurve) { + return []; + } + + const holders = await ctx.db + .query("bondingCurveHolders") + .withIndex("by_curve", (q) => q.eq("bondingCurveId", bondingCurve._id)) + .collect(); + + // Group holders by balance range + const ranges = [ + { label: "0-100", min: 0, max: 100 }, + { label: "100-1K", min: 100, max: 1000 }, + { label: "1K-10K", min: 1000, max: 10000 }, + { label: "10K-100K", min: 10000, max: 100000 }, + { label: "100K+", min: 100000, max: Infinity }, + ]; + + const distribution = ranges.map((range) => ({ + range: range.label, + count: holders.filter((h) => h.balance >= range.min && h.balance < range.max).length, + })); + + return distribution; + }, +}); + +// Get social metrics +export const getSocialMetrics = query({ + args: { + tokenId: v.id("memeCoins"), + }, + handler: async (ctx, args) => { + const socialShares = await ctx.db + .query("socialShares") + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) + .collect(); + + const comments = await ctx.db + .query("tokenComments") + .withIndex("by_token", (q) => q.eq("tokenId", args.tokenId)) + .collect(); + + const reactions = await ctx.db + .query("tokenReactions") + .withIndex("by_token", (q) => q.eq("tokenId", args.tokenId)) + .collect(); + + return { + totalShares: socialShares.length, + totalComments: comments.length, + totalReactions: reactions.length, + platforms: { + twitter: socialShares.filter((s) => s.platform === "twitter").length, + telegram: socialShares.filter((s) => s.platform === "telegram").length, + discord: socialShares.filter((s) => s.platform === "discord").length, + }, + reactionCounts: { + rocket: reactions.filter((r) => r.reaction === "rocket").length, + fire: reactions.filter((r) => r.reaction === "fire").length, + gem: reactions.filter((r) => r.reaction === "gem").length, + moon: reactions.filter((r) => r.reaction === "moon").length, + }, + }; + }, +}); + export const fetchRealTimeAnalytics = internalAction({ args: { coinId: v.id("memeCoins"), diff --git a/convex/bondingCurveApi.ts b/convex/bondingCurveApi.ts index 84248f3..a512da1 100644 --- a/convex/bondingCurveApi.ts +++ b/convex/bondingCurveApi.ts @@ -330,4 +330,7 @@ export const getUserBalance = query({ usdBalance, }; }, -}); \ No newline at end of file +}); + +// Alias for compatibility +export const getBondingCurve = getCurveData; \ No newline at end of file diff --git a/convex/schema.ts b/convex/schema.ts index 1f43fe1..35717ff 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -74,7 +74,7 @@ const applicationTables = { // Bonding curve state for each token bondingCurves: defineTable({ coinId: v.id("memeCoins"), - tokenId: v.id("memeCoins"), // Added for compatibility + tokenId: v.optional(v.id("memeCoins")), // Made optional for backward compatibility currentSupply: v.number(), currentPrice: v.number(), reserveBalance: v.number(), From 529f11b8eb0e67071312f5430fa7d3b7e892827d Mon Sep 17 00:00:00 2001 From: jmanhype Date: Mon, 26 May 2025 21:11:11 -0500 Subject: [PATCH 2/5] fix: correct table names for social metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use 'comments' instead of 'tokenComments' - Use 'reactions' instead of 'tokenReactions' - Update reaction type field name from 'reaction' to 'type' - Add all reaction types from schema (diamond, trash, bear) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- convex/analytics.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/convex/analytics.ts b/convex/analytics.ts index d965672..8959efa 100644 --- a/convex/analytics.ts +++ b/convex/analytics.ts @@ -412,12 +412,12 @@ export const getSocialMetrics = query({ .collect(); const comments = await ctx.db - .query("tokenComments") + .query("comments") .withIndex("by_token", (q) => q.eq("tokenId", args.tokenId)) .collect(); const reactions = await ctx.db - .query("tokenReactions") + .query("reactions") .withIndex("by_token", (q) => q.eq("tokenId", args.tokenId)) .collect(); @@ -431,10 +431,12 @@ export const getSocialMetrics = query({ discord: socialShares.filter((s) => s.platform === "discord").length, }, reactionCounts: { - rocket: reactions.filter((r) => r.reaction === "rocket").length, - fire: reactions.filter((r) => r.reaction === "fire").length, - gem: reactions.filter((r) => r.reaction === "gem").length, - moon: reactions.filter((r) => r.reaction === "moon").length, + rocket: reactions.filter((r) => r.type === "rocket").length, + fire: reactions.filter((r) => r.type === "fire").length, + diamond: reactions.filter((r) => r.type === "diamond").length, + moon: reactions.filter((r) => r.type === "moon").length, + trash: reactions.filter((r) => r.type === "trash").length, + bear: reactions.filter((r) => r.type === "bear").length, }, }; }, From f221aa1b4233def2c2effdc2549d109510681e52 Mon Sep 17 00:00:00 2001 From: jmanhype Date: Mon, 26 May 2025 21:19:08 -0500 Subject: [PATCH 3/5] fix: remove byTokenId index and migration helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove byTokenId index since tokenId is now optional - Add migration helpers to fix existing documents - Keep coinId as required field for proper indexing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- convex/analytics.ts | 2 +- convex/bondingCurve.ts | 12 ++++---- convex/dex/graduation.ts | 4 +-- convex/dex/liquidityManager.ts | 2 +- convex/fixBondingCurves.ts | 30 +++++++++++++++++++ .../migrations/addTokenIdToBondingCurves.ts | 25 ++++++++++++++++ convex/runMigration.ts | 11 +++++++ convex/schema.ts | 4 +-- 8 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 convex/fixBondingCurves.ts create mode 100644 convex/migrations/addTokenIdToBondingCurves.ts create mode 100644 convex/runMigration.ts diff --git a/convex/analytics.ts b/convex/analytics.ts index 8959efa..2af7699 100644 --- a/convex/analytics.ts +++ b/convex/analytics.ts @@ -112,7 +112,7 @@ export const getTokenAnalytics = query({ // Get bonding curve for real blockchain data const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); let holderDistribution: any[] = []; diff --git a/convex/bondingCurve.ts b/convex/bondingCurve.ts index 4c204e6..3a091b1 100644 --- a/convex/bondingCurve.ts +++ b/convex/bondingCurve.ts @@ -292,7 +292,7 @@ export const getBondingCurve = query({ handler: async (ctx, args) => { return await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); }, }); @@ -306,7 +306,7 @@ export const calculateBuyAmount = query({ handler: async (ctx, args) => { const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (!bondingCurve) throw new Error("Bonding curve not found"); @@ -362,7 +362,7 @@ export const calculateSellReturn = query({ handler: async (ctx, args) => { const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (!bondingCurve) throw new Error("Bonding curve not found"); @@ -424,7 +424,7 @@ export const recordBuyTransaction = mutation({ // Get bonding curve const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (!bondingCurve) throw new Error("Bonding curve not found"); @@ -518,7 +518,7 @@ export const recordSellTransaction = mutation({ // Get bonding curve const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (!bondingCurve) throw new Error("Bonding curve not found"); @@ -613,7 +613,7 @@ export const updateBondingCurveState = internalMutation({ handler: async (ctx, args) => { const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (!bondingCurve) throw new Error("Bonding curve not found"); diff --git a/convex/dex/graduation.ts b/convex/dex/graduation.ts index 59d1b15..e43baae 100644 --- a/convex/dex/graduation.ts +++ b/convex/dex/graduation.ts @@ -40,7 +40,7 @@ export const checkGraduationEligibility = action({ // Get bonding curve data const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (!bondingCurve) { @@ -125,7 +125,7 @@ export const graduateToken = action({ // Get bonding curve data const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (!bondingCurve) throw new Error("No bonding curve data"); diff --git a/convex/dex/liquidityManager.ts b/convex/dex/liquidityManager.ts index 7377431..591e224 100644 --- a/convex/dex/liquidityManager.ts +++ b/convex/dex/liquidityManager.ts @@ -241,7 +241,7 @@ export const recordLiquidityProvision = internalMutation({ // Update bonding curve with DEX pool info if not already set const bondingCurve = await ctx.db .query("bondingCurves") - .withIndex("byTokenId", (q) => q.eq("tokenId", args.tokenId)) + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) .first(); if (bondingCurve && !bondingCurve.dexPoolAddress) { diff --git a/convex/fixBondingCurves.ts b/convex/fixBondingCurves.ts new file mode 100644 index 0000000..bb011e2 --- /dev/null +++ b/convex/fixBondingCurves.ts @@ -0,0 +1,30 @@ +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const fixBondingCurvesSchema = mutation({ + args: {}, + handler: async (ctx) => { + // Get all bonding curves + const bondingCurves = await ctx.db + .query("bondingCurves") + .collect(); + + let fixed = 0; + for (const curve of bondingCurves) { + // If tokenId is missing, add it with the same value as coinId + if (!curve.tokenId && curve.coinId) { + await ctx.db.patch(curve._id, { + tokenId: curve.coinId, + }); + fixed++; + console.log(`Fixed bonding curve ${curve._id} by adding tokenId`); + } + } + + return { + message: `Fixed ${fixed} bonding curves`, + total: bondingCurves.length, + fixed + }; + }, +}); \ No newline at end of file diff --git a/convex/migrations/addTokenIdToBondingCurves.ts b/convex/migrations/addTokenIdToBondingCurves.ts new file mode 100644 index 0000000..5a788a3 --- /dev/null +++ b/convex/migrations/addTokenIdToBondingCurves.ts @@ -0,0 +1,25 @@ +import { internalMutation } from "../_generated/server"; + +export const addTokenIdToBondingCurves = internalMutation({ + args: {}, + handler: async (ctx) => { + // Get all bonding curves without tokenId + const bondingCurves = await ctx.db + .query("bondingCurves") + .collect(); + + let updated = 0; + for (const curve of bondingCurves) { + // If tokenId is missing, set it to coinId + if (!curve.tokenId && curve.coinId) { + await ctx.db.patch(curve._id, { + tokenId: curve.coinId, + }); + updated++; + } + } + + console.log(`Updated ${updated} bonding curves with tokenId`); + return { updated }; + }, +}); \ No newline at end of file diff --git a/convex/runMigration.ts b/convex/runMigration.ts new file mode 100644 index 0000000..f058e25 --- /dev/null +++ b/convex/runMigration.ts @@ -0,0 +1,11 @@ +import { mutation } from "./_generated/server"; +import { internal } from "./_generated/api"; + +export const runBondingCurveMigration = mutation({ + args: {}, + handler: async (ctx) => { + // Run the migration + const result = await ctx.runMutation(internal.migrations.addTokenIdToBondingCurves, {}); + return result; + }, +}); \ No newline at end of file diff --git a/convex/schema.ts b/convex/schema.ts index 35717ff..34f3888 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -95,8 +95,8 @@ const applicationTables = { transactions: v.optional(v.number()), // Total transaction count }) .index("by_coin", ["coinId"]) - .index("by_active", ["isActive"]) - .index("byTokenId", ["tokenId"]), // Added for compatibility with existing code + .index("by_active", ["isActive"]), + // Removed byTokenId index since tokenId is optional // Bonding curve transactions bondingCurveTransactions: defineTable({ From e37b88802d1be6b84a6eca69f2653f27a73aa749 Mon Sep 17 00:00:00 2001 From: jmanhype Date: Mon, 26 May 2025 21:42:48 -0500 Subject: [PATCH 4/5] fix: convert createMemeCoin db calls to mutations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add helper mutations for rate limit and duplicate checks - Add createCoinRecord mutation for database insert - Fix 'Cannot read properties of undefined' error by using proper action patterns - Actions cannot directly access ctx.db, must use runMutation/runQuery 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- convex/memeCoins.ts | 87 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/convex/memeCoins.ts b/convex/memeCoins.ts index 70e67e7..10aa2aa 100644 --- a/convex/memeCoins.ts +++ b/convex/memeCoins.ts @@ -132,6 +132,71 @@ export const checkRateLimit = query({ }, }); +// Helper mutation to check rate limit +export const checkRateLimit = internalMutation({ + args: { + userId: v.string(), + }, + handler: async (ctx, args) => { + const now = Date.now(); + const oneDayAgo = now - 24 * 60 * 60 * 1000; + + const recentCoins = await ctx.db + .query("memeCoins") + .withIndex("by_creator", (q) => q.eq("creatorId", args.userId)) + .filter((q) => q.gt(q.field("_creationTime"), oneDayAgo)) + .collect(); + + return { count: recentCoins.length }; + }, +}); + +// Helper mutation to check duplicate symbol +export const checkDuplicateSymbol = internalMutation({ + args: { + symbol: v.string(), + }, + handler: async (ctx, args) => { + const existingCoin = await ctx.db + .query("memeCoins") + .filter((q) => q.eq(q.field("symbol"), args.symbol.toUpperCase())) + .first(); + + return { exists: !!existingCoin }; + }, +}); + +// Helper mutation to create coin +export const createCoinRecord = internalMutation({ + args: { + name: v.string(), + symbol: v.string(), + initialSupply: v.number(), + canMint: v.boolean(), + canBurn: v.boolean(), + postQuantumSecurity: v.boolean(), + creatorId: v.string(), + description: v.optional(v.string()), + blockchain: v.union(v.literal("ethereum"), v.literal("solana"), v.literal("bsc")), + }, + handler: async (ctx, args) => { + const coinId = await ctx.db.insert("memeCoins", { + name: args.name, + symbol: args.symbol.toUpperCase(), + initialSupply: args.initialSupply, + canMint: args.canMint, + canBurn: args.canBurn, + postQuantumSecurity: args.postQuantumSecurity, + creatorId: args.creatorId, + description: args.description, + status: "pending", + blockchain: args.blockchain, + }); + + return coinId; + }, +}); + // Create a new meme coin export const createMemeCoin = action({ args: { @@ -165,26 +230,20 @@ export const createMemeCoin = action({ } // Check rate limit - const now = Date.now(); - const oneDayAgo = now - 24 * 60 * 60 * 1000; - - const recentCoins = await ctx.db - .query("memeCoins") - .withIndex("by_creator", (q) => q.eq("creatorId", userId)) - .filter((q) => q.gt(q.field("_creationTime"), oneDayAgo)) - .collect(); + const rateLimitResult = await ctx.runMutation(internal.memeCoins.checkRateLimit, { + userId, + }); - if (recentCoins.length >= 3) { + if (rateLimitResult.count >= 3) { throw new Error("Rate limit exceeded. You can only create 3 coins per day."); } // Check for duplicate symbol - const existingCoin = await ctx.db - .query("memeCoins") - .filter((q) => q.eq(q.field("symbol"), args.symbol.toUpperCase())) - .first(); + const duplicateResult = await ctx.runMutation(internal.memeCoins.checkDuplicateSymbol, { + symbol: args.symbol, + }); - if (existingCoin) { + if (duplicateResult.exists) { throw new Error("A coin with this symbol already exists"); } From 6c957085e5f52c237cbc2a9c24ba65a70d55e680 Mon Sep 17 00:00:00 2001 From: jmanhype Date: Tue, 27 May 2025 00:53:22 -0500 Subject: [PATCH 5/5] fix: resolve bonding curve deployment and trading interface issues - Split bonding curve bytecode into two env vars to handle Convex's 8KB limit - Fix constructor args to match BondingCurve contract (token, feeRecipient) - Add constructor to ABI definition for proper contract deployment - Fix trading interface crash by reordering query hooks and using useAction - Add authentication wrappers to protected routes - Fix getUserHoldings to return single object instead of array - Add missing database index for bonding curve transactions - Create bytecode extraction script for easier deployment setup - Add simulated trading for development environment - Update test mocks to include bondingCurve API This enables real blockchain deployment of bonding curves for token trading. --- .mcp.json | 60 ++++++ convex/_generated/api.d.ts | 8 + convex/blockchain/bondingCurveIntegration.ts | 24 ++- convex/bondingCurve.ts | 186 +++++++++++++++++-- convex/memeCoins.ts | 109 ++++++++--- convex/revenue/creatorRevenue.ts | 2 +- convex/schema.ts | 12 +- convex/simulatedTrading.ts | 139 ++++++++++++++ scripts/extract-bondingcurve-bytecode.js | 26 +++ src/App.tsx | 33 +++- src/components/CoinCard.tsx | 37 +++- src/components/TokenAnalytics.tsx | 55 +++--- src/components/TradingInterface.tsx | 167 ++++++++++------- src/test/setup.ts | 3 + 14 files changed, 705 insertions(+), 156 deletions(-) create mode 100644 .mcp.json create mode 100644 convex/simulatedTrading.ts create mode 100644 scripts/extract-bondingcurve-bytecode.js diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..ae5b1eb --- /dev/null +++ b/.mcp.json @@ -0,0 +1,60 @@ +{ + "mcpServers": { + "neon-server": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@neondatabase/mcp-server-neon", + "start", + "napi_ctcjmic0tobrl04904dwx7qgpyfn96nl5l9ss8b1le4qlgz3816x6y2wnfejtgek" + ], + "env": {} + }, + "browser-tools": { + "type": "stdio", + "command": "/opt/homebrew/bin/node", + "args": [ + "/Users/speed/.npm/_npx/dcd6f0de5842aab6/node_modules/@agentdeskai/browser-tools-mcp/dist/mcp-server.js", + "run" + ], + "env": {} + }, + "github": { + "type": "stdio", + "command": "/Users/speed/github-mcp-server/github-mcp-server", + "args": [ + "stdio" + ], + "env": {} + }, + "taskmaster-ai": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "--package=task-master-ai", + "task-master-ai" + ], + "env": {} + }, + "exa": { + "type": "stdio", + "command": "/opt/homebrew/bin/npx", + "args": [ + "exa-mcp-server" + ], + "env": {} + }, + "web-eval-agent": { + "type": "stdio", + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/Operative-Sh/web-eval-agent.git", + "webEvalAgent" + ], + "env": {} + } + } +} \ No newline at end of file diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index ed389d4..7573739 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -46,9 +46,11 @@ import type * as dex_uniswapV3 from "../dex/uniswapV3.js"; import type * as fairLaunch from "../fairLaunch.js"; import type * as fees_feeManager from "../fees/feeManager.js"; import type * as fees_initializeFees from "../fees/initializeFees.js"; +import type * as fixBondingCurves from "../fixBondingCurves.js"; import type * as http from "../http.js"; import type * as jobQueue from "../jobQueue.js"; import type * as memeCoins from "../memeCoins.js"; +import type * as migrations_addTokenIdToBondingCurves from "../migrations/addTokenIdToBondingCurves.js"; import type * as monitoring_alerts from "../monitoring/alerts.js"; import type * as monitoring_auditLog from "../monitoring/auditLog.js"; import type * as monitoring_metrics from "../monitoring/metrics.js"; @@ -60,8 +62,10 @@ import type * as oracles_priceOracle from "../oracles/priceOracle.js"; import type * as reflections from "../reflections.js"; import type * as revenue_creatorRevenue from "../revenue/creatorRevenue.js"; import type * as router from "../router.js"; +import type * as runMigration from "../runMigration.js"; import type * as security_multiSigActions from "../security/multiSigActions.js"; import type * as security_multiSigQueries from "../security/multiSigQueries.js"; +import type * as simulatedTrading from "../simulatedTrading.js"; import type * as social_comments from "../social/comments.js"; import type * as social_discord from "../social/discord.js"; import type * as social_formatter from "../social/formatter.js"; @@ -116,9 +120,11 @@ declare const fullApi: ApiFromModules<{ fairLaunch: typeof fairLaunch; "fees/feeManager": typeof fees_feeManager; "fees/initializeFees": typeof fees_initializeFees; + fixBondingCurves: typeof fixBondingCurves; http: typeof http; jobQueue: typeof jobQueue; memeCoins: typeof memeCoins; + "migrations/addTokenIdToBondingCurves": typeof migrations_addTokenIdToBondingCurves; "monitoring/alerts": typeof monitoring_alerts; "monitoring/auditLog": typeof monitoring_auditLog; "monitoring/metrics": typeof monitoring_metrics; @@ -130,8 +136,10 @@ declare const fullApi: ApiFromModules<{ reflections: typeof reflections; "revenue/creatorRevenue": typeof revenue_creatorRevenue; router: typeof router; + runMigration: typeof runMigration; "security/multiSigActions": typeof security_multiSigActions; "security/multiSigQueries": typeof security_multiSigQueries; + simulatedTrading: typeof simulatedTrading; "social/comments": typeof social_comments; "social/discord": typeof social_discord; "social/formatter": typeof social_formatter; diff --git a/convex/blockchain/bondingCurveIntegration.ts b/convex/blockchain/bondingCurveIntegration.ts index 3e53af6..b2d049c 100644 --- a/convex/blockchain/bondingCurveIntegration.ts +++ b/convex/blockchain/bondingCurveIntegration.ts @@ -4,6 +4,9 @@ import { ethers } from "ethers"; // Bonding Curve Contract ABI (essential functions) const BONDING_CURVE_ABI = [ + // Constructor + "constructor(address _token, address _feeRecipient)", + // Functions "function buy(uint256 minTokensOut) external payable returns (uint256 tokensReceived)", "function sell(uint256 tokenAmount, uint256 minEthOut) external returns (uint256 ethReceived)", "function calculateBuyReturn(uint256 ethAmount) external view returns (uint256 tokenAmount)", @@ -246,7 +249,9 @@ export const deployBondingCurveContract = internalAction({ ? process.env.ETHEREUM_RPC_URL : process.env.BSC_RPC_URL; - const privateKey = process.env.DEPLOYER_PRIVATE_KEY; + const privateKey = args.blockchain === "ethereum" + ? process.env.ETHEREUM_DEPLOYER_PRIVATE_KEY + : process.env.BSC_DEPLOYER_PRIVATE_KEY; if (!rpcUrl || !privateKey) { throw new Error(`Missing configuration for ${args.blockchain}`); @@ -256,11 +261,15 @@ export const deployBondingCurveContract = internalAction({ const signer = new ethers.Wallet(privateKey, provider); // Bonding Curve contract bytecode (compiled from BondingCurve.sol) - const BONDING_CURVE_BYTECODE = process.env.BONDING_CURVE_BYTECODE; + // Concatenate bytecode parts (split due to Convex env var size limit) + const bytecodePart1 = process.env.BONDING_CURVE_BYTECODE_PART1; + const bytecodePart2 = process.env.BONDING_CURVE_BYTECODE_PART2; - if (!BONDING_CURVE_BYTECODE) { + if (!bytecodePart1 || !bytecodePart2) { throw new Error("Bonding curve bytecode not configured"); } + + const BONDING_CURVE_BYTECODE = bytecodePart1 + bytecodePart2; // Deploy bonding curve contract const BondingCurveFactory = new ethers.ContractFactory( @@ -269,13 +278,14 @@ export const deployBondingCurveContract = internalAction({ signer ); - const graduationTarget = args.graduationTarget || 100; // 100 ETH default - const feePercent = args.feePercent || 100; // 1% default + // Get fee recipient address (treasury or deployer) + const feeRecipient = process.env.MAINNET_TREASURY_ADDRESS || + process.env.FEE_COLLECTOR_ADDRESS || + signer.address; // Default to deployer if no treasury set const bondingCurve = await BondingCurveFactory.deploy( args.tokenAddress, - ethers.parseEther(graduationTarget.toString()), - feePercent + feeRecipient ); await bondingCurve.waitForDeployment(); diff --git a/convex/bondingCurve.ts b/convex/bondingCurve.ts index 3a091b1..339cb10 100644 --- a/convex/bondingCurve.ts +++ b/convex/bondingCurve.ts @@ -41,7 +41,7 @@ export const initializeBondingCurve = mutation({ createdAt: Date.now(), }); - // Schedule bonding curve contract deployment + // Schedule bonding curve deployment await ctx.scheduler.runAfter(0, internal.bondingCurve.deployBondingCurveContract, { bondingCurveId, coinId: args.coinId, @@ -53,7 +53,7 @@ export const initializeBondingCurve = mutation({ }); // Deploy bonding curve smart contract (internal action) -export const deployBondingCurveContract: any = action({ +export const deployBondingCurveContract = action({ args: { bondingCurveId: v.id("bondingCurves"), coinId: v.id("memeCoins"), @@ -69,20 +69,12 @@ export const deployBondingCurveContract: any = action({ } // Deploy bonding curve contract - const result = await ctx.runAction(internal.blockchain.bondingCurveDeployment.deployBondingCurve, { + const result = await ctx.runAction(internal.blockchain.bondingCurveIntegration.deployBondingCurveContract, { tokenAddress: deployment.contractAddress, tokenId: args.coinId, blockchain: deployment.blockchain, }); - // Initialize bonding curve with tokens - await ctx.runAction(internal.blockchain.bondingCurveDeployment.initializeBondingCurve, { - bondingCurveAddress: result.bondingCurveAddress, - tokenAddress: deployment.contractAddress, - initialSupply: args.initialSupply * 0.4, // 40% of supply for bonding curve - blockchain: deployment.blockchain, - }); - // Update bonding curve record await ctx.runMutation(internal.bondingCurve.updateBondingCurveAddress, { bondingCurveId: args.bondingCurveId, @@ -141,7 +133,7 @@ export const recordBondingCurveDeployment = internalMutation({ }); // Execute buy order on bonding curve -export const buyTokens: any = action({ +export const buyTokens = action({ args: { tokenId: v.id("memeCoins"), ethAmount: v.number(), @@ -159,8 +151,9 @@ export const buyTokens: any = action({ throw new Error("Bonding curve not active"); } - if (!bondingCurve.contractAddress) { - throw new Error("Bonding curve contract not deployed"); + // Check if bonding curve contract is deployed + if (!bondingCurve.dexPoolAddress || bondingCurve.dexPoolAddress.startsWith('sim_')) { + throw new Error("Bonding curve contract not deployed. Please deploy it first."); } // Calculate buy amount using bonding curve formula @@ -180,7 +173,7 @@ export const buyTokens: any = action({ // Get transaction data for frontend execution const txData = await ctx.runAction(internal.blockchain.bondingCurveIntegration.executeBondingCurveBuy, { - bondingCurveAddress: bondingCurve.contractAddress, + bondingCurveAddress: bondingCurve.dexPoolAddress, tokenId: args.tokenId, blockchain: deployment.blockchain, buyer: userId.subject, @@ -199,7 +192,7 @@ export const buyTokens: any = action({ }); // Execute sell order on bonding curve -export const sellTokens: any = action({ +export const sellTokens = action({ args: { tokenId: v.id("memeCoins"), tokenAmount: v.number(), @@ -217,8 +210,9 @@ export const sellTokens: any = action({ throw new Error("Bonding curve not active"); } - if (!bondingCurve.contractAddress) { - throw new Error("Bonding curve contract not deployed"); + // Check if bonding curve contract is deployed + if (!bondingCurve.dexPoolAddress || bondingCurve.dexPoolAddress.startsWith('sim_')) { + throw new Error("Bonding curve contract not deployed. Please deploy it first."); } // Calculate sell return using bonding curve formula @@ -238,7 +232,7 @@ export const sellTokens: any = action({ // Get transaction data for frontend execution const txData = await ctx.runAction(internal.blockchain.bondingCurveIntegration.executeBondingCurveSell, { - bondingCurveAddress: bondingCurve.contractAddress, + bondingCurveAddress: bondingCurve.dexPoolAddress, tokenId: args.tokenId, blockchain: deployment.blockchain, seller: userId.subject, @@ -660,4 +654,158 @@ export const reset24hVolume = internalMutation({ }); } }, +}); + +// Get user holdings for all bonding curves +export const getAllUserHoldings = query({ + args: { + coinId: v.optional(v.id("memeCoins")), + userId: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) { + return []; + } + + // Get all holdings for this user + const holdings = await ctx.db + .query("bondingCurveHolders") + .withIndex("by_user", (q) => q.eq("user", identity.tokenIdentifier)) + .collect(); + + // Get bonding curve and coin data for each holding + const holdingsWithData = await Promise.all( + holdings.map(async (holding) => { + const bondingCurve = await ctx.db.get(holding.bondingCurveId); + if (!bondingCurve) return null; + + // Filter by coinId if specified + if (args.coinId && bondingCurve.coinId !== args.coinId) { + return null; + } + + const coin = await ctx.db.get(bondingCurve.coinId); + if (!coin) return null; + + return { + ...holding, + bondingCurve, + coin, + currentValue: holding.balance * bondingCurve.currentPrice, + }; + }) + ); + + return holdingsWithData.filter(Boolean); + }, +}); + +// Deploy bonding curve for an existing token +export const deployBondingCurveForToken = mutation({ + args: { + coinId: v.id("memeCoins"), + }, + handler: async (ctx, args) => { + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error("Not authenticated"); + + // Get the coin to verify ownership + const coin = await ctx.db.get(args.coinId); + if (!coin) throw new Error("Coin not found"); + if (coin.creatorId !== userId) throw new Error("Not authorized"); + + // Get the bonding curve + const bondingCurve = await ctx.db + .query("bondingCurves") + .withIndex("by_coin", (q) => q.eq("coinId", args.coinId)) + .first(); + + if (!bondingCurve) throw new Error("Bonding curve not found"); + + // Check if already deployed + if (bondingCurve.dexPoolAddress && !bondingCurve.dexPoolAddress.startsWith('sim_')) { + throw new Error("Bonding curve already deployed"); + } + + // Schedule deployment + await ctx.scheduler.runAfter(0, internal.bondingCurve.deployBondingCurveContract, { + bondingCurveId: bondingCurve._id, + coinId: args.coinId, + initialSupply: coin.initialSupply, + }); + + return { success: true, message: "Bonding curve deployment scheduled" }; + }, +}); + +// Get user holdings for a specific coin (returns single object for TradingInterface) +export const getUserHoldings = query({ + args: { + coinId: v.optional(v.id("memeCoins")), + userId: v.optional(v.string()), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) { + return null; + } + + if (!args.coinId) { + return null; + } + + // Get bonding curve for this coin + const bondingCurve = await ctx.db + .query("bondingCurves") + .withIndex("by_coin", (q) => q.eq("coinId", args.coinId)) + .first(); + + if (!bondingCurve) { + return null; + } + + // Get user's holding for this bonding curve + const holding = await ctx.db + .query("bondingCurveHolders") + .withIndex("by_curve_user", (q) => + q.eq("bondingCurveId", bondingCurve._id).eq("user", identity.tokenIdentifier) + ) + .first(); + + if (!holding || holding.balance <= 0) { + return null; + } + + // Calculate average buy price and P&L + const transactions = await ctx.db + .query("bondingCurveTransactions") + .withIndex("by_curve_user", (q) => + q.eq("bondingCurveId", bondingCurve._id).eq("user", identity.tokenIdentifier) + ) + .collect(); + + let totalCost = 0; + let totalBought = 0; + + for (const tx of transactions) { + if (tx.type === "buy") { + totalCost += tx.amountIn || 0; + totalBought += tx.tokensOut || 0; + } + } + + const averageBuyPrice = totalBought > 0 ? totalCost / totalBought : 0; + const currentValue = holding.balance * bondingCurve.currentPrice; + const unrealizedPnL = currentValue - (holding.balance * averageBuyPrice); + const unrealizedPnLPercent = averageBuyPrice > 0 ? (unrealizedPnL / (holding.balance * averageBuyPrice)) * 100 : 0; + + return { + balance: holding.balance, + currentValue, + averageBuyPrice, + unrealizedPnL, + unrealizedPnLPercent, + }; + }, }); \ No newline at end of file diff --git a/convex/memeCoins.ts b/convex/memeCoins.ts index 10aa2aa..fc9a942 100644 --- a/convex/memeCoins.ts +++ b/convex/memeCoins.ts @@ -25,8 +25,6 @@ export const listMemeCoins = query({ .withIndex("by_coin", (q) => q.eq("coinId", coin._id)) .first(); - const creator = await ctx.db.get(coin.creatorId); - // Get bonding curve info if exists const bondingCurve = await ctx.db .query("bondingCurves") @@ -36,7 +34,7 @@ export const listMemeCoins = query({ return { ...coin, deployment, - creatorName: creator?.name || creator?.email || "Anonymous", + creatorName: "Anonymous", // Using anonymous since creatorId is now a string bondingCurve: bondingCurve ? { isActive: bondingCurve.isActive, currentPrice: bondingCurve.currentPrice, @@ -62,11 +60,17 @@ export const getUserMemeCoins = query({ throw new Error("Not authenticated"); } + // Debug: Log the userId being used for the query + console.log("getUserMemeCoins - userId:", userId); + console.log("getUserMemeCoins - userId as string:", userId as string); + const coins = await ctx.db .query("memeCoins") - .withIndex("by_creator", (q) => q.eq("creatorId", userId)) + .withIndex("by_creator", (q) => q.eq("creatorId", userId as string)) .order("desc") .collect(); + + console.log("getUserMemeCoins - coins found:", coins.length); // Get deployment info for each coin const coinsWithDeployments = await Promise.all( @@ -88,6 +92,7 @@ export const getUserMemeCoins = query({ bondingCurve: bondingCurve ? { isActive: bondingCurve.isActive, currentPrice: bondingCurve.currentPrice, + hasContract: !!bondingCurve.dexPoolAddress && !bondingCurve.dexPoolAddress.startsWith('sim_'), marketCap: bondingCurve.currentSupply * bondingCurve.currentPrice, progress: (bondingCurve.currentSupply * bondingCurve.currentPrice / 100000) * 100, totalVolume: bondingCurve.totalVolume, @@ -116,7 +121,7 @@ export const checkRateLimit = query({ // Count coins created in the last 24 hours const recentCoins = await ctx.db .query("memeCoins") - .withIndex("by_creator", (q) => q.eq("creatorId", userId)) + .withIndex("by_creator", (q) => q.eq("creatorId", userId as string)) .filter((q) => q.gt(q.field("_creationTime"), oneDayAgo)) .collect(); @@ -133,7 +138,7 @@ export const checkRateLimit = query({ }); // Helper mutation to check rate limit -export const checkRateLimit = internalMutation({ +export const internalCheckRateLimit = internalMutation({ args: { userId: v.string(), }, @@ -152,7 +157,7 @@ export const checkRateLimit = internalMutation({ }); // Helper mutation to check duplicate symbol -export const checkDuplicateSymbol = internalMutation({ +export const internalCheckDuplicateSymbol = internalMutation({ args: { symbol: v.string(), }, @@ -166,8 +171,34 @@ export const checkDuplicateSymbol = internalMutation({ }, }); +// Helper mutation to get or create user +export const internalGetOrCreateUser = internalMutation({ + args: { + subject: v.string(), + }, + handler: async (ctx, args) => { + // Check if user exists + const existingUser = await ctx.db + .query("users") + .withIndex("by_subject", (q) => q.eq("subject", args.subject)) + .first(); + + if (existingUser) { + return existingUser._id; + } + + // Create new user + const userId = await ctx.db.insert("users", { + subject: args.subject, + createdAt: Date.now(), + }); + + return userId; + }, +}); + // Helper mutation to create coin -export const createCoinRecord = internalMutation({ +export const internalCreateCoinRecord = internalMutation({ args: { name: v.string(), symbol: v.string(), @@ -210,11 +241,18 @@ export const createMemeCoin = action({ blockchain: v.union(v.literal("ethereum"), v.literal("solana"), v.literal("bsc")), }, handler: async (ctx, args) => { - const identity = await ctx.auth.getUserIdentity(); - if (!identity) { + // Use getAuthUserId to get the consistent user ID + const userId = await getAuthUserId(ctx); + if (!userId) { throw new Error("Not authenticated"); } - const userId = identity.subject; + + // Also get identity for debugging + const identity = await ctx.auth.getUserIdentity(); + + // Debug: Log both IDs to see the difference + console.log("createMemeCoin - userId from getAuthUserId:", userId); + console.log("createMemeCoin - identity.subject:", identity?.subject); // Validate inputs if (args.name.length < 2 || args.name.length > 50) { @@ -230,8 +268,8 @@ export const createMemeCoin = action({ } // Check rate limit - const rateLimitResult = await ctx.runMutation(internal.memeCoins.checkRateLimit, { - userId, + const rateLimitResult = await ctx.runMutation(internal.memeCoins.internalCheckRateLimit, { + userId: userId as string, }); if (rateLimitResult.count >= 3) { @@ -239,7 +277,7 @@ export const createMemeCoin = action({ } // Check for duplicate symbol - const duplicateResult = await ctx.runMutation(internal.memeCoins.checkDuplicateSymbol, { + const duplicateResult = await ctx.runMutation(internal.memeCoins.internalCheckDuplicateSymbol, { symbol: args.symbol, }); @@ -255,23 +293,22 @@ export const createMemeCoin = action({ }); // Create the meme coin - const coinId = await ctx.runMutation(internal.memeCoins.createCoinRecord, { + const coinId = await ctx.runMutation(internal.memeCoins.internalCreateCoinRecord, { name: args.name, symbol: args.symbol.toUpperCase(), initialSupply: args.initialSupply, canMint: args.canMint, canBurn: args.canBurn, postQuantumSecurity: args.postQuantumSecurity, - creatorId: userId, + creatorId: userId as string, // Convert ID to string for schema compatibility description: args.description, - status: "pending", blockchain: args.blockchain, }); // Record fee collection after coin creation if (feeData.fee > 0) { await ctx.runMutation(internal.fees.feeManager.recordFeeCollection, { - userId, + userId: userId as string, tokenId: coinId, feeType: 0, // TOKEN_CREATION amount: feeData.fee, @@ -300,7 +337,7 @@ export const createMemeCoin = action({ // Initialize revenue tracking for the creator await ctx.runMutation(internal.revenue.creatorRevenue.initializeRevenue, { tokenId: coinId, - creatorId: userId, + creatorId: userId as string, blockchain: args.blockchain, }); @@ -373,8 +410,6 @@ export const getCoinDetails = query({ .withIndex("by_coin", (q) => q.eq("coinId", args.coinId)) .first(); - const creator = await ctx.db.get(coin.creatorId); - // Get latest analytics const analytics = await ctx.db .query("analytics") @@ -386,7 +421,7 @@ export const getCoinDetails = query({ ...coin, deployment, analytics, - creatorName: creator?.name || creator?.email || "Anonymous", + creatorName: "Anonymous", // Using anonymous since creatorId is now a string }; }, }); @@ -516,7 +551,7 @@ export const getByContractAddress = internalQuery({ }); // Internal mutation to create coin record -export const createCoinRecord = internalMutation({ +export const internalCreateCoinRecordOld = internalMutation({ args: { name: v.string(), symbol: v.string(), @@ -543,6 +578,34 @@ export const createCoinRecord = internalMutation({ }, }); +// Debug query to inspect all coins and their creators +export const debugAllCoins = query({ + args: {}, + handler: async (ctx) => { + const userId = await getAuthUserId(ctx); + console.log("debugAllCoins - current userId:", userId); + + const allCoins = await ctx.db + .query("memeCoins") + .order("desc") + .take(10); + + const coinsWithInfo = allCoins.map(coin => ({ + _id: coin._id, + name: coin.name, + symbol: coin.symbol, + creatorId: coin.creatorId, + status: coin.status, + _creationTime: coin._creationTime, + })); + + return { + currentUserId: userId, + coins: coinsWithInfo, + }; + }, +}); + // Internal mutation to update token status after DEX listing export const updateTokenStatus = internalMutation({ args: { diff --git a/convex/revenue/creatorRevenue.ts b/convex/revenue/creatorRevenue.ts index 477fe48..de0040f 100644 --- a/convex/revenue/creatorRevenue.ts +++ b/convex/revenue/creatorRevenue.ts @@ -21,7 +21,7 @@ export const initializeRevenue = internalMutation({ if (!existing) { await ctx.db.insert("creatorRevenue", { - creatorId: args.creatorId as Id<"users">, + creatorId: args.creatorId, tokenId: args.tokenId, totalEarned: 0, totalWithdrawn: 0, diff --git a/convex/schema.ts b/convex/schema.ts index 34f3888..d49db41 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -11,11 +11,12 @@ const applicationTables = { canMint: v.boolean(), canBurn: v.boolean(), postQuantumSecurity: v.boolean(), - creatorId: v.id("users"), + creatorId: v.string(), // Temporarily back to string until migration description: v.optional(v.string()), logoUrl: v.optional(v.string()), status: v.union(v.literal("pending"), v.literal("deployed"), v.literal("failed"), v.literal("graduated")), multiSigWallet: v.optional(v.string()), + blockchain: v.optional(v.union(v.literal("ethereum"), v.literal("solana"), v.literal("bsc"))), // Added for compatibility }) .index("by_creator", ["creatorId"]) .index("by_status", ["status"]), @@ -113,7 +114,8 @@ const applicationTables = { }) .index("by_curve", ["bondingCurveId"]) .index("by_user", ["user"]) - .index("by_timestamp", ["timestamp"]), + .index("by_timestamp", ["timestamp"]) + .index("by_curve_user", ["bondingCurveId", "user"]), // Bonding curve token holders bondingCurveHolders: defineTable({ @@ -608,7 +610,7 @@ const applicationTables = { // Creator revenue tracking creatorRevenue: defineTable({ - creatorId: v.id("users"), + creatorId: v.string(), // Changed to string to match memeCoins tokenId: v.id("memeCoins"), totalEarned: v.number(), totalWithdrawn: v.number(), @@ -621,7 +623,7 @@ const applicationTables = { // Revenue transactions revenueTransactions: defineTable({ - creatorId: v.id("users"), + creatorId: v.string(), // Changed to string to match memeCoins tokenId: v.id("memeCoins"), type: v.union( v.literal("trading_fee"), @@ -661,7 +663,7 @@ const applicationTables = { // Withdrawal requests withdrawalRequests: defineTable({ - creatorId: v.id("users"), + creatorId: v.string(), // Changed to string to match memeCoins amount: v.number(), status: v.union( v.literal("pending"), diff --git a/convex/simulatedTrading.ts b/convex/simulatedTrading.ts new file mode 100644 index 0000000..c0894e6 --- /dev/null +++ b/convex/simulatedTrading.ts @@ -0,0 +1,139 @@ +import { v } from "convex/values"; +import { mutation, query, action } from "./_generated/server"; +import { api } from "./_generated/api"; +import { getAuthUserId } from "@convex-dev/auth/server"; + +// Simulated buy tokens without requiring blockchain deployment +export const simulatedBuyTokens = action({ + args: { + tokenId: v.id("memeCoins"), + ethAmount: v.number(), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Not authenticated"); + + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error("Not authenticated"); + + // Get bonding curve details + const bondingCurve = await ctx.runQuery(api.bondingCurve.getBondingCurve, { + tokenId: args.tokenId, + }); + + if (!bondingCurve || !bondingCurve.isActive) { + throw new Error("Bonding curve not active"); + } + + // Calculate buy amount using bonding curve formula + const buyAmount = await ctx.runQuery(api.bondingCurve.calculateBuyAmount, { + tokenId: args.tokenId, + amountInUSD: args.ethAmount, + }); + + // Check fair launch restrictions + const fairLaunchCheck = await ctx.runQuery(api.fairLaunch.checkPurchaseAllowed, { + tokenId: args.tokenId, + buyer: identity.email || identity.tokenIdentifier, + amount: buyAmount.tokensOut, + }); + + if (!fairLaunchCheck.allowed) { + throw new Error(fairLaunchCheck.reason); + } + + // Record the buy transaction + await ctx.runMutation(api.bondingCurve.recordBuyTransaction, { + tokenId: args.tokenId, + user: identity.tokenIdentifier, + ethAmount: args.ethAmount, + tokenAmount: buyAmount.tokensOut, + price: buyAmount.newPrice, + }); + + // Return simulated transaction result + return { + success: true, + txHash: `sim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + expectedTokens: buyAmount.tokensOut, + avgPrice: buyAmount.avgPrice, + priceImpact: buyAmount.priceImpact, + }; + }, +}); + +// Simulated sell tokens without requiring blockchain deployment +export const simulatedSellTokens = action({ + args: { + tokenId: v.id("memeCoins"), + tokenAmount: v.number(), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Not authenticated"); + + const userId = await getAuthUserId(ctx); + if (!userId) throw new Error("Not authenticated"); + + // Get bonding curve details + const bondingCurve = await ctx.runQuery(api.bondingCurve.getBondingCurve, { + tokenId: args.tokenId, + }); + + if (!bondingCurve || !bondingCurve.isActive) { + throw new Error("Bonding curve not active"); + } + + // Get user holdings + const userHoldings = await ctx.runQuery(api.bondingCurve.getUserHoldings, { + coinId: args.tokenId, + userId: userId, + }); + + if (!userHoldings || userHoldings.balance < args.tokenAmount) { + throw new Error("Insufficient balance"); + } + + // Calculate sell return using bonding curve formula + const sellReturn = await ctx.runQuery(api.bondingCurve.calculateSellReturn, { + tokenId: args.tokenId, + tokenAmount: args.tokenAmount, + }); + + // Record the sell transaction + await ctx.runMutation(api.bondingCurve.recordSellTransaction, { + tokenId: args.tokenId, + user: identity.tokenIdentifier, + tokenAmount: args.tokenAmount, + ethAmount: sellReturn.amountOut, + price: sellReturn.newPrice, + }); + + // Return simulated transaction result + return { + success: true, + txHash: `sim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + expectedEth: sellReturn.amountOut, + avgPrice: sellReturn.avgPrice, + priceImpact: sellReturn.priceImpact, + }; + }, +}); + +// Check if trading is simulated for a token +export const isSimulatedTrading = query({ + args: { + tokenId: v.id("memeCoins"), + }, + handler: async (ctx, args) => { + const bondingCurve = await ctx.db + .query("bondingCurves") + .withIndex("by_coin", (q) => q.eq("coinId", args.tokenId)) + .first(); + + if (!bondingCurve) return false; + + // If no contract address or starts with 'sim_', it's simulated + return !bondingCurve.dexPoolAddress || bondingCurve.dexPoolAddress.startsWith('sim_'); + }, +}); \ No newline at end of file diff --git a/scripts/extract-bondingcurve-bytecode.js b/scripts/extract-bondingcurve-bytecode.js new file mode 100644 index 0000000..4575828 --- /dev/null +++ b/scripts/extract-bondingcurve-bytecode.js @@ -0,0 +1,26 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Read the compiled BondingCurve contract +const artifactPath = path.join(__dirname, '../artifacts/contracts/BondingCurve.sol/BondingCurve.json'); +const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8')); + +// Extract bytecode +const bytecode = artifact.bytecode; + +// Write to .env.local +const envPath = path.join(__dirname, '../.env.local'); +const envContent = fs.readFileSync(envPath, 'utf8'); + +// Replace or add BONDING_CURVE_BYTECODE +const updatedContent = envContent.replace( + /BONDING_CURVE_BYTECODE=.*/, + `BONDING_CURVE_BYTECODE=${bytecode}` +); + +fs.writeFileSync(envPath, updatedContent); +console.log('BondingCurve bytecode extracted and added to .env.local!'); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index b9f9bf9..d623306 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,9 +18,36 @@ export default function App() { return ( - } /> - } /> - } /> + + + + + + + + + } /> + + + + + + + + + } /> + + + + + + + + + } /> } /> diff --git a/src/components/CoinCard.tsx b/src/components/CoinCard.tsx index 10e88c8..65eb5f4 100644 --- a/src/components/CoinCard.tsx +++ b/src/components/CoinCard.tsx @@ -1,8 +1,10 @@ -import { useQuery } from "convex/react"; +import { useQuery, useMutation } from "convex/react"; import { api } from "../../convex/_generated/api"; import { Id } from "../../convex/_generated/dataModel"; import { Link } from "react-router-dom"; -import { TrendingUp, Users, Activity } from "lucide-react"; +import { TrendingUp, Users, Activity, Zap } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; interface CoinCardProps { coin: { @@ -43,11 +45,26 @@ interface CoinCardProps { } export function CoinCard({ coin, showAnalytics = false }: CoinCardProps) { + const [isDeploying, setIsDeploying] = useState(false); + const deployBondingCurve = useMutation(api.bondingCurve.deployBondingCurveForToken); + const analytics = useQuery( api.analytics.getCoinAnalytics, showAnalytics && coin.status === "deployed" ? { coinId: coin._id } : "skip" ); + const handleDeployBondingCurve = async () => { + try { + setIsDeploying(true); + await deployBondingCurve({ coinId: coin._id }); + toast.success("Bonding curve deployment scheduled! This may take a few minutes."); + } catch (error: any) { + toast.error(error.message || "Failed to deploy bonding curve"); + } finally { + setIsDeploying(false); + } + }; + const getStatusColor = (status: string) => { switch (status) { case "deployed": @@ -222,7 +239,19 @@ export function CoinCard({ coin, showAnalytics = false }: CoinCardProps) { Supply: {coin.initialSupply.toLocaleString()}
- {coin.bondingCurve?.isActive && ( + {/* Deploy Bonding Curve button */} + {coin.status === "deployed" && coin.bondingCurve && !coin.bondingCurve.hasContract && ( + + )} + + {coin.bondingCurve?.isActive && coin.bondingCurve?.hasContract && ( )} + {coin.status === "deployed" && ( )} + {!coin.bondingCurve?.isActive && coin.status === "graduated" && ( Trading on DEX diff --git a/src/components/TokenAnalytics.tsx b/src/components/TokenAnalytics.tsx index 0cfca46..9be6c57 100644 --- a/src/components/TokenAnalytics.tsx +++ b/src/components/TokenAnalytics.tsx @@ -65,24 +65,19 @@ export default function TokenAnalytics({ tokenId }: TokenAnalyticsProps) { const metrics = useMemo(() => { if (!analytics || !token) return null; - const latestData = analytics.data[analytics.data.length - 1]; - const firstData = analytics.data[0]; - - const priceChange = latestData && firstData - ? ((latestData.price - firstData.price) / firstData.price) * 100 - : 0; - - const volumeTotal = analytics.data.reduce((sum, d) => sum + d.volume24h, 0); + // Use the metrics directly from the analytics response + const analyticsMetrics = analytics.metrics; + if (!analyticsMetrics) return null; return { - currentPrice: latestData?.price || 0, - priceChange, - marketCap: latestData?.marketCap || 0, - volume24h: latestData?.volume24h || 0, - volumeTotal, - holders: latestData?.holders || 0, - transactions: analytics.data.reduce((sum, d) => sum + d.transactions24h, 0), - avgTransactionSize: volumeTotal / (analytics.data.reduce((sum, d) => sum + d.transactions24h, 0) || 1), + currentPrice: analyticsMetrics.price || 0, + priceChange: analyticsMetrics.priceChange24h || 0, + marketCap: analyticsMetrics.marketCap || 0, + volume24h: analyticsMetrics.volume24h || 0, + volumeTotal: analyticsMetrics.volume24h || 0, // Use 24h volume as total for now + holders: analyticsMetrics.holders || 0, + transactions: analyticsMetrics.transactions24h || 0, + avgTransactionSize: (analyticsMetrics.volume24h || 0) / (analyticsMetrics.transactions24h || 1), }; }, [analytics, token]); @@ -94,13 +89,13 @@ export default function TokenAnalytics({ tokenId }: TokenAnalyticsProps) { ); } - const chartData = analytics.data.map(d => ({ + const chartData = analytics.charts?.price?.map((d: any) => ({ time: new Date(d.timestamp).toLocaleDateString(), price: d.price, - volume: d.volume24h, - holders: d.holders, - marketCap: d.marketCap, - })); + volume: analytics.charts?.volume?.find((v: any) => v.timestamp === d.timestamp)?.volume || 0, + holders: analytics.charts?.holders?.find((h: any) => h.timestamp === d.timestamp)?.holders || 0, + marketCap: d.price * (analytics.metrics?.totalSupply || 1000000000), + })) || []; const pieData = holderDistribution ? [ { name: "Top 10", value: holderDistribution.top10Percentage, color: "#8884d8" }, @@ -283,11 +278,11 @@ export default function TokenAnalytics({ tokenId }: TokenAnalyticsProps) {
Largest Holder - {holderDistribution.largestHolder.toFixed(2)}% + {(holderDistribution.largestHolder || 0).toFixed(2)}%
Gini Coefficient - {holderDistribution.giniCoefficient.toFixed(3)} + {(holderDistribution.giniCoefficient || 0).toFixed(3)}
@@ -326,7 +321,7 @@ export default function TokenAnalytics({ tokenId }: TokenAnalyticsProps) {

- {socialMetrics.sentimentScore.toFixed(0)}% + {(socialMetrics.sentimentScore || 0).toFixed(0)}%

Positive Sentiment

@@ -379,7 +374,7 @@ export default function TokenAnalytics({ tokenId }: TokenAnalyticsProps) { {formatNumber(trade.tokenAmount)} - ${trade.price.toFixed(6)} + ${(trade.price || 0).toFixed(6)} ${formatNumber(trade.ethAmount)} @@ -409,30 +404,30 @@ export default function TokenAnalytics({ tokenId }: TokenAnalyticsProps) {

- {((bondingCurve.currentSupply / 800000000) * 100).toFixed(1)}% Complete + {(((bondingCurve.currentSupply || 0) / 800000000) * 100).toFixed(1)}% Complete

Reserve Balance

- {bondingCurve.reserveBalance.toFixed(4)} ETH + {(bondingCurve.reserveBalance || 0).toFixed(4)} ETH

Current Price

- ${bondingCurve.currentPrice.toFixed(6)} + ${(bondingCurve.currentPrice || 0).toFixed(6)}

- {bondingCurve.isActive && bondingCurve.currentSupply >= 800000000 * 0.9 && ( + {bondingCurve.isActive && (bondingCurve.currentSupply || 0) >= 800000000 * 0.9 && (
diff --git a/src/components/TradingInterface.tsx b/src/components/TradingInterface.tsx index d431ddf..e5a60c5 100644 --- a/src/components/TradingInterface.tsx +++ b/src/components/TradingInterface.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useQuery, useMutation } from "convex/react"; +import { useQuery, useMutation, useAction } from "convex/react"; import { api } from "../../convex/_generated/api"; import { Id } from "../../convex/_generated/dataModel"; import { toast } from "sonner"; @@ -50,14 +50,6 @@ export default function TradingInterface({ coinId }: TradingInterfaceProps) { api.bondingCurve.getUserHoldings, user ? { coinId, userId: user._id } : "skip" ); - - // Check fair launch restrictions - const fairLaunchCheck = useQuery( - api.fairLaunch.checkPurchaseAllowed, - user && amount && parseFloat(amount) > 0 && tradeType === "buy" && buyPreview - ? { tokenId: coinId, buyer: user.email || "", amount: buyPreview.tokensReceived } - : "skip" - ); // Calculate buy/sell amounts const buyPreview = useQuery( @@ -73,10 +65,19 @@ export default function TradingInterface({ coinId }: TradingInterfaceProps) { ? { tokenId: coinId, tokenAmount: parseFloat(amount) } : "skip" ); + + // Check fair launch restrictions + const fairLaunchCheck = useQuery( + api.fairLaunch.checkPurchaseAllowed, + user && amount && parseFloat(amount) > 0 && tradeType === "buy" && buyPreview + ? { tokenId: coinId, buyer: user.email || "", amount: buyPreview.tokensOut || buyPreview.tokensReceived || 0 } + : "skip" + ); - // Trading mutations - const buyTokens = useMutation(api.bondingCurve.buyTokens); - const sellTokens = useMutation(api.bondingCurve.sellTokens); + + // Trading actions + const buyTokens = useAction(api.bondingCurve.buyTokens); + const sellTokens = useAction(api.bondingCurve.sellTokens); // Check wallet connection on mount useEffect(() => { @@ -119,7 +120,10 @@ export default function TradingInterface({ coinId }: TradingInterfaceProps) { return; } - if (!walletConnected) { + // Check if this is simulated trading + const isSimulated = !bondingCurve.dexPoolAddress || bondingCurve.dexPoolAddress.startsWith('sim_'); + + if (!isSimulated && !walletConnected) { toast.error("Please connect your wallet first"); return; } @@ -134,58 +138,80 @@ export default function TradingInterface({ coinId }: TradingInterfaceProps) { ethAmount: parseFloat(amount), }); - // Execute blockchain transaction - const txHash = await web3Service.executeTransaction(result.txData); - - toast.success( -
-

Successfully bought ~{result.expectedTokens} {coin.symbol}

- - View transaction - -
- ); - } else { - // For sell, need to approve token first - const deployment = await api.memeCoins.getDeployment({ coinId }); - if (!deployment || !bondingCurve.contractAddress) { - throw new Error("Contract addresses not found"); + // Execute blockchain transaction or handle simulated + let txHash; + if (result.txData.simulated) { + txHash = result.txData.txHash; + toast.success( +
+

Successfully bought ~{result.expectedTokens} {coin.symbol}

+

Simulated transaction: {txHash}

+
+ ); + } else { + txHash = await web3Service.executeTransaction(result.txData); + toast.success( +
+

Successfully bought ~{result.expectedTokens} {coin.symbol}

+ + View transaction + +
+ ); } - - // Approve bonding curve to spend tokens - await web3Service.approveToken( - deployment.contractAddress, - bondingCurve.contractAddress, - amount - ); - + } else { // Get sell transaction data const result = await sellTokens({ tokenId: coinId, tokenAmount: parseFloat(amount), }); - - // Execute blockchain transaction - const txHash = await web3Service.executeTransaction(result.txData); - toast.success( -
-

Successfully sold for ~{result.expectedEth} ETH

- - View transaction - -
- ); + // Only handle approval for non-simulated trades + if (!result.txData.simulated) { + const deployment = await api.memeCoins.getDeployment({ coinId }); + if (!deployment || !bondingCurve.dexPoolAddress) { + throw new Error("Contract addresses not found"); + } + + // Approve bonding curve to spend tokens + await web3Service.approveToken( + deployment.contractAddress, + bondingCurve.dexPoolAddress, + amount + ); + } + + // Execute blockchain transaction or handle simulated + let txHash; + if (result.txData.simulated) { + txHash = result.txData.txHash; + toast.success( +
+

Successfully sold for ~{result.expectedEth} ETH

+

Simulated transaction: {txHash}

+
+ ); + } else { + txHash = await web3Service.executeTransaction(result.txData); + toast.success( +
+

Successfully sold for ~{result.expectedEth} ETH

+ + View transaction + +
+ ); + } } setAmount(""); } catch (error: any) { @@ -256,6 +282,17 @@ export default function TradingInterface({ coinId }: TradingInterfaceProps) { {/* Trading Form */}
+ {/* Simulated Trading Notice */} + {bondingCurve.dexPoolAddress?.startsWith('sim_') && ( +
+ +
+

Simulated Trading Mode

+

This token is using simulated trading for development/testing. No real blockchain transactions will occur.

+
+
+ )} + {/* Trade Type Selector */}
Average Price - ${(tradeType === "buy" ? buyPreview?.avgPrice : sellPreview?.avgPrice || 0).toFixed(6)} + ${((tradeType === "buy" ? buyPreview?.avgPrice : sellPreview?.avgPrice) || 0).toFixed(6)}
Price Impact 5 + ((tradeType === "buy" ? buyPreview?.priceImpact : sellPreview?.priceImpact) || 0) > 5 ? "text-red-600" : "text-green-600" }`}> - {(tradeType === "buy" ? buyPreview?.priceImpact : sellPreview?.priceImpact || 0).toFixed(2)}% + {((tradeType === "buy" ? buyPreview?.priceImpact : sellPreview?.priceImpact) || 0).toFixed(2)}%
@@ -376,7 +413,7 @@ export default function TradingInterface({ coinId }: TradingInterfaceProps) { )} {/* Trade Button */} - {!walletConnected ? ( + {!walletConnected && (!bondingCurve.dexPoolAddress || !bondingCurve.dexPoolAddress.startsWith('sim_')) ? (