/ src / services / userService.js
userService.js
   1  import crypto from "crypto";
   2  import bs58 from "bs58";
   3  import { ethers } from "ethers";
   4  import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
   5  import { SuiClient } from "@mysten/sui/client";
   6  import nacl from "tweetnacl";
   7  import { User } from "@/models/User.js";
   8  import { logger } from "@/middleware/logging.js";
   9  import config from "@/config.js";
  10  import { hdWalletDeriver } from "@/services/hdWalletDeriver.js";
  11  import { dollarsToCents, centsToDollars } from "@/utils/fiatUtils.js";
  12  import { hashIpAddress, checkIpHashAbuse } from "@/utils/ipHasher.js";
  13  import { timingSafeCompare, addTimingNoise, validateWalletAddress } from "@/utils/cryptoUtils.js";
  14  import cache, { CacheEvents } from "@/utils/cache.js";
  15  import networks, { getEnabledNetworksForType } from "@/data/crypto/networks.js";
  16  
  17  const validateSignature = (signature, chainType) => {
  18    if (!signature || typeof signature !== "string") {
  19      return false;
  20    } else if (signature.length > 512) {
  21      return false;
  22    } else if (chainType === "solana" || chainType === "sui" || chainType === "bitcoin") {
  23      return /^[1-9A-HJ-NP-Za-km-z]{87,88}$/.test(signature);
  24    } else if (chainType === "evm") {
  25      return /^0x[a-fA-F0-9]{130}$/.test(signature);
  26    } else {
  27      return false;
  28    }
  29  };
  30  
  31  class SignatureVerifier {
  32    static verifySolana(message, signature, publicKey) {
  33      try {
  34        if (!message || !signature || !publicKey) {
  35          return false;
  36        } else if (typeof message !== "string" || message.length > 10000) {
  37          return false;
  38        } else {
  39          const messageBytes = new TextEncoder().encode(message);
  40          const signatureBytes = bs58.decode(signature);
  41          const publicKeyBytes = bs58.decode(publicKey);
  42          if (signatureBytes.length !== 64) {
  43            return false;
  44          } else if (publicKeyBytes.length !== 32) {
  45            return false;
  46          } else {
  47            return nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes);
  48          }
  49        }
  50      } catch (error) {
  51        logger.error("Solana signature verification failed", { message: error.message });
  52        return false;
  53      }
  54    }
  55  
  56    static verifyEthereum(message, signature, address) {
  57      try {
  58        if (!message || !signature || !address) {
  59          return false;
  60        } else if (typeof message !== "string" || message.length > 10000) {
  61          return false;
  62        } else {
  63          const recoveredAddress = ethers.verifyMessage(message, signature);
  64          return timingSafeCompare(recoveredAddress.toLowerCase(), address.toLowerCase());
  65        }
  66      } catch (error) {
  67        logger.error("Ethereum signature verification failed", { message: error.message });
  68        return false;
  69      }
  70    }
  71  
  72    static verify(message, signature, address, chainType) {
  73      if (!validateWalletAddress(address, chainType)) {
  74        return false;
  75      } else if (!validateSignature(signature, chainType)) {
  76        return false;
  77      } else if (chainType === "solana" || chainType === "sui" || chainType === "bitcoin") {
  78        return this.verifySolana(message, signature, address);
  79      } else if (chainType === "evm") {
  80        return this.verifyEthereum(message, signature, address);
  81      } else {
  82        return false;
  83      }
  84    }
  85  }
  86  
  87  class SessionManager {
  88    constructor() {
  89      this.sessions = new Map();
  90      this.sessionDuration = config.security.sessionDuration || 24 * 60 * 60 * 1000;
  91      this.maxSessions = config.security.maxSessions || 100000;
  92      this.sessionsByUser = new Map();
  93      this.maxSessionsPerUser = config.security.maxSessionsPerUser || 5;
  94      this.sessionRetryAttempts = new Map();
  95      this.maxFailedLogins = config.security.maxFailedLogins || 5;
  96      this.accountLockDuration = config.security.accountLockDuration || 30 * 60 * 1000;
  97      this.maxRetryAttempts = config.retries.few;
  98      this.retryLockoutDuration = config.security.retryLockoutDuration || 5 * 60 * 1000;
  99    }
 100  
 101    generateSecureToken() {
 102      return crypto.randomBytes(32).toString("hex");
 103    }
 104  
 105    hashToken(token) {
 106      return crypto.createHash("sha256").update(token).digest("hex");
 107    }
 108  
 109    createSession(userId, walletAddress) {
 110      const userIdStr = userId.toString();
 111      const userSessions = this.sessionsByUser.get(userIdStr) || [];
 112      if (userSessions.length >= this.maxSessionsPerUser) {
 113        const oldestToken = userSessions.shift();
 114        this.sessions.delete(oldestToken);
 115      }
 116      if (this.sessions.size >= this.maxSessions) {
 117        const keysToDelete = Array.from(this.sessions.keys()).slice(0, 1000);
 118        keysToDelete.forEach(k => {
 119          const session = this.sessions.get(k);
 120          if (session && session.userId) {
 121            const userSessionList = this.sessionsByUser.get(session.userId);
 122            if (userSessionList) {
 123              const index = userSessionList.indexOf(k);
 124              if (index > -1) {
 125                userSessionList.splice(index, 1);
 126              }
 127            }
 128          }
 129          this.sessions.delete(k);
 130        });
 131      }
 132      const sessionToken = crypto.randomBytes(32).toString("hex");
 133      const tokenHash = this.hashToken(sessionToken);
 134      const expiry = new Date(Date.now() + this.sessionDuration);
 135      const session = {
 136        userId: userIdStr,
 137        walletAddress,
 138        tokenHash,
 139        expiry,
 140        createdAt: new Date(),
 141      };
 142      this.sessions.set(tokenHash, session);
 143      userSessions.push(tokenHash);
 144      this.sessionsByUser.set(userIdStr, userSessions);
 145      setTimeout(() => {
 146        this.deleteSession(sessionToken);
 147      }, this.sessionDuration);
 148      return { sessionToken, expiry };
 149    }
 150  
 151    validateSession(sessionToken) {
 152      if (!sessionToken || typeof sessionToken !== "string") {
 153        return { valid: false, error: "Invalid token" };
 154      }
 155      if (sessionToken.length < 32 || sessionToken.length > 128) {
 156        return { valid: false, error: "Invalid token format" };
 157      }
 158  
 159      const clientIdentifier = sessionToken.substring(0, 8);
 160      const retryTracker = this.sessionRetryAttempts.get(clientIdentifier);
 161      if (retryTracker && retryTracker.attempts >= this.maxRetryAttempts) {
 162        const timeSinceLastAttempt = Date.now() - retryTracker.lastAttempt;
 163        if (timeSinceLastAttempt < this.retryLockoutDuration) {
 164          const remainingTime = Math.ceil((this.retryLockoutDuration - timeSinceLastAttempt) / 1000);
 165          return { valid: false, error: `Too many attempts. Try again in ${remainingTime} seconds` };
 166        } else {
 167          this.sessionRetryAttempts.delete(clientIdentifier);
 168        }
 169      }
 170  
 171      const tokenHash = this.hashToken(sessionToken);
 172      const session = this.sessions.get(tokenHash);
 173      if (!session) {
 174        this.trackRetryAttempt(clientIdentifier);
 175        return { valid: false, error: "Session not found" };
 176      }
 177      if (session.expiry && new Date() > session.expiry) {
 178        this.deleteSession(sessionToken);
 179        this.trackRetryAttempt(clientIdentifier);
 180        return { valid: false, error: "Session expired" };
 181      }
 182  
 183      if (retryTracker) {
 184        this.sessionRetryAttempts.delete(clientIdentifier);
 185      }
 186  
 187      session.lastAccessed = new Date();
 188      return { valid: true, userId: session.userId, walletAddress: session.walletAddress };
 189    }
 190  
 191    deleteSession(sessionToken) {
 192      if (!sessionToken) {
 193        return;
 194      }
 195      const tokenHash = this.hashToken(sessionToken);
 196      const session = this.sessions.get(tokenHash);
 197      if (session) {
 198        const userIdStr = session.userId.toString();
 199        const userTokens = this.sessionsByUser.get(userIdStr) || [];
 200        const idx = userTokens.indexOf(tokenHash);
 201        if (idx > -1) {
 202          userTokens.splice(idx, 1);
 203        }
 204      }
 205      this.sessions.delete(tokenHash);
 206    }
 207  
 208    deleteAllUserSessions(userId) {
 209      const userIdStr = userId.toString();
 210      const userTokens = this.sessionsByUser.get(userIdStr) || [];
 211      userTokens.forEach(tokenHash => this.sessions.delete(tokenHash));
 212      this.sessionsByUser.delete(userIdStr);
 213    }
 214  
 215    refreshSession(sessionToken) {
 216      if (!sessionToken) {
 217        return null;
 218      }
 219      const tokenHash = this.hashToken(sessionToken);
 220      const session = this.sessions.get(tokenHash);
 221      if (!session) {
 222        return null;
 223      }
 224      const expiry = new Date(Date.now() + this.sessionDuration);
 225      session.expiry = expiry;
 226      session.lastActivity = new Date();
 227      return { sessionToken, expiry };
 228    }
 229  
 230    restoreSession(sessionToken, userId, walletAddress, expiry) {
 231      if (!sessionToken || !userId) {
 232        return;
 233      }
 234      const userIdStr = userId.toString();
 235      const tokenHash = this.hashToken(sessionToken);
 236      this.sessions.set(tokenHash, {
 237        userId: userIdStr,
 238        walletAddress,
 239        expiry: expiry || new Date(Date.now() + this.sessionDuration),
 240        createdAt: new Date(),
 241        lastActivity: new Date(),
 242      });
 243      const userTokens = this.sessionsByUser.get(userIdStr) || [];
 244      if (!userTokens.includes(tokenHash)) {
 245        userTokens.push(tokenHash);
 246        this.sessionsByUser.set(userIdStr, userTokens);
 247      }
 248    }
 249  
 250    getActiveSessionsCount() {
 251      return this.sessions.size;
 252    }
 253  
 254    cleanupExpiredSessions() {
 255      const now = new Date();
 256      const expiredSessions = [];
 257  
 258      for (const [tokenHash, session] of this.sessions.entries()) {
 259        if (now > session.expiry) {
 260          expiredSessions.push(tokenHash);
 261          const userIdStr = session.userId.toString();
 262          const userTokens = this.sessionsByUser.get(userIdStr) || [];
 263          const idx = userTokens.indexOf(tokenHash);
 264          if (idx > -1) {
 265            userTokens.splice(idx, 1);
 266          }
 267        }
 268      }
 269  
 270      expiredSessions.forEach(tokenHash => this.sessions.delete(tokenHash));
 271  
 272      const nowTime = Date.now();
 273      for (const [identifier, tracker] of this.sessionRetryAttempts.entries()) {
 274        if (nowTime - tracker.lastAttempt > this.retryLockoutDuration) {
 275          this.sessionRetryAttempts.delete(identifier);
 276        }
 277      }
 278    }
 279  
 280    trackRetryAttempt(clientIdentifier) {
 281      const tracker = this.sessionRetryAttempts.get(clientIdentifier) || { attempts: 0, lastAttempt: 0 };
 282      tracker.attempts++;
 283      tracker.lastAttempt = Date.now();
 284      this.sessionRetryAttempts.set(clientIdentifier, tracker);
 285    }
 286  }
 287  
 288  const sessionManager = new SessionManager();
 289  
 290  setInterval(() => {
 291    sessionManager.cleanupExpiredSessions();
 292  }, config.security.sessionCleanupInterval);
 293  
 294  const generateRandomUsername = () => {
 295    const prefix = "user";
 296    const randomBytes = crypto.randomBytes(6);
 297    const randomSuffix = randomBytes.toString("hex").slice(0, 10);
 298    return `${prefix}${randomSuffix}`;
 299  };
 300  
 301  const generateUniqueUsername = async () => {
 302    const maxAttempts = 50;
 303    for (let attempts = 0; attempts < maxAttempts; attempts++) {
 304      const username = generateRandomUsername();
 305      const existingUser = await User.findOne({ username }).select("_id").lean();
 306      if (!existingUser) {
 307        return username;
 308      }
 309    }
 310    const timestamp = Date.now().toString(36);
 311    const random = crypto.randomBytes(4).toString("hex");
 312    return `user${timestamp}${random}`.slice(0, 16);
 313  };
 314  
 315  export class AuthService {
 316    constructor() {
 317      this.challenges = new Map();
 318      this.challengeDuration = config.security.challengeDuration || 5 * 60 * 1000;
 319      this.sessionManager = sessionManager;
 320      this.maxFailedLogins = config.security.maxFailedLogins || 5;
 321      this.accountLockDuration = config.security.accountLockDuration || 30 * 60 * 1000;
 322      this.challengeRateLimit = new Map();
 323      this.maxChallengesPerIp = config.security.maxChallengesPerIp || 10;
 324      this.challengeRateLimitWindow = config.security.challengeRateLimitWindow || 60000;
 325    }
 326  
 327    getChainTypeFromWalletType(walletType) {
 328      const solanaWallets = ["phantom", "solflare", "brave"];
 329      return solanaWallets.includes(walletType) ? "solana" : "evm";
 330    }
 331  
 332    checkChallengeRateLimit(identifier) {
 333      const now = Date.now();
 334      let tracker = this.challengeRateLimit.get(identifier);
 335      if (!tracker) {
 336        tracker = { count: 0, windowStart: now };
 337        this.challengeRateLimit.set(identifier, tracker);
 338      }
 339      if (now - tracker.windowStart > this.challengeRateLimitWindow) {
 340        tracker.count = 0;
 341        tracker.windowStart = now;
 342      }
 343      if (tracker.count >= this.maxChallengesPerIp) {
 344        return false;
 345      }
 346      tracker.count++;
 347      return true;
 348    }
 349  
 350    generateChallenge(walletAddress, walletType = null, clientIp = null) {
 351      if (!walletAddress || typeof walletAddress !== "string") {
 352        return null;
 353      }
 354  
 355      const chainType = this.getChainTypeFromWalletType(walletType);
 356      if (!validateWalletAddress(walletAddress, chainType)) {
 357        return null;
 358      } else if (clientIp && !this.checkChallengeRateLimit(clientIp)) {
 359        return null;
 360      }
 361  
 362      if (this.challenges.size >= this.maxChallenges) {
 363        const now = Date.now();
 364        const staleThreshold = 300000;
 365        const entries = Array.from(this.challenges.entries());
 366  
 367        const staleEntries = entries.filter(([_, challenge]) =>
 368          now - challenge.timestamp > staleThreshold,
 369        );
 370  
 371        if (staleEntries.length > 0) {
 372          staleEntries.forEach(([key]) => this.challenges.delete(key));
 373        } else {
 374          const oldestEntries = entries
 375            .sort((a, b) => a[1].timestamp - b[1].timestamp)
 376            .slice(0, 5000);
 377          oldestEntries.forEach(([key]) => this.challenges.delete(key));
 378        }
 379      }
 380      const nonce = crypto.randomBytes(32).toString("hex");
 381      const timestamp = Date.now();
 382      const serverSecret = crypto.randomBytes(16).toString("hex");
 383      const message = `Sign this message to authenticate with Clushy:\n\nTimestamp: ${timestamp}\nNonce: ${nonce}\n\nThis signature will not trigger any blockchain transaction or cost any gas fees.`;
 384      const challengeData = {
 385        message,
 386        nonce,
 387        timestamp,
 388        expiry: timestamp + this.challengeDuration,
 389        serverSecret,
 390        attempts: 0,
 391        maxAttempts: 3,
 392        walletAddress,
 393        walletType,
 394      };
 395      const walletKey = chainType === "solana" ? walletAddress : walletAddress.toLowerCase();
 396  
 397      logger.info(`Storing challenge for wallet: ${walletAddress}, type: ${walletType}, key: ${walletKey}`);
 398  
 399      this.challenges.set(walletKey, challengeData);
 400      setTimeout(() => {
 401        this.challenges.delete(walletKey);
 402      }, this.challengeDuration);
 403      return { message, nonce, timestamp };
 404    }
 405  
 406    async authenticateWallet(walletAddress, signature, walletType, clientInfo = {}, providedAddresses = {}, chainType = null) {
 407      try {
 408        await addTimingNoise(20, 50);
 409        if (!walletAddress || typeof walletAddress !== "string") {
 410          await addTimingNoise();
 411          return { success: false, error: "Invalid wallet address" };
 412        }
 413        if (!chainType) {
 414          chainType = this.getChainTypeFromWalletType(walletType);
 415        }
 416  
 417        if (!validateWalletAddress(walletAddress, chainType)) {
 418          await addTimingNoise();
 419          return { success: false, error: "Invalid wallet address format" };
 420        } else if (!validateSignature(signature, chainType)) {
 421          await addTimingNoise();
 422          return { success: false, error: "Invalid signature format" };
 423        }
 424  
 425        const { getEnabledWallets } = await import("../data/crypto/wallets.js");
 426        const enabledWallets = getEnabledWallets();
 427        const validWalletTypes = enabledWallets.map(w => w.id);
 428        if (!validWalletTypes.includes(walletType)) {
 429          await addTimingNoise();
 430          return { success: false, error: "Invalid wallet type" };
 431        }
 432  
 433        const walletKey = (walletType === "phantom" || walletType === "solflare" || walletType === "brave")
 434          ? walletAddress
 435          : walletAddress.toLowerCase();
 436  
 437        logger.info(`Looking up challenge for wallet: ${walletAddress}, type: ${walletType}, key: ${walletKey}`);
 438        logger.info(`Current challenges in map: ${Array.from(this.challenges.keys()).join(", ")}`);
 439  
 440        const challenge = this.challenges.get(walletKey);
 441        if (!challenge) {
 442          await addTimingNoise();
 443          return { success: false, error: "Challenge not found or expired" };
 444        } else if (challenge.attempts >= challenge.maxAttempts) {
 445          this.challenges.delete(walletKey);
 446          await addTimingNoise();
 447          return { success: false, error: "Too many verification attempts" };
 448        }
 449  
 450        challenge.attempts++;
 451        if (Date.now() > challenge.expiry) {
 452          this.challenges.delete(walletKey);
 453          await addTimingNoise();
 454          return { success: false, error: "Challenge expired" };
 455        }
 456        let user = await User.findOne({ walletAddress: walletKey });
 457        if (user && user.security?.accountLocked) {
 458          if (user.security.accountLockedUntil && new Date() > user.security.accountLockedUntil) {
 459            await User.updateOne(
 460              { _id: user._id },
 461              { $set: { "security.accountLocked": false, "security.failedLoginAttempts": 0 } },
 462            );
 463            user.security.accountLocked = false;
 464            user.security.failedLoginAttempts = 0;
 465          } else {
 466            await addTimingNoise();
 467            return { success: false, error: "Account is temporarily locked" };
 468          }
 469        }
 470        if (user && user.isBanned) {
 471          await addTimingNoise();
 472          return { success: false, error: "Account is suspended" };
 473        }
 474        const verified = SignatureVerifier.verify(challenge.message, signature, walletAddress, chainType);
 475        if (!verified) {
 476          if (user) {
 477            const failedAttempts = (user.security?.failedLoginAttempts || 0) + 1;
 478            const updateData = {
 479              "security.failedLoginAttempts": failedAttempts,
 480              "security.lastFailedLogin": new Date(),
 481            };
 482            if (failedAttempts >= this.maxFailedLogins) {
 483              updateData["security.accountLocked"] = true;
 484              updateData["security.accountLockedUntil"] = new Date(Date.now() + this.accountLockDuration);
 485            }
 486            await User.updateOne({ _id: user._id }, { $set: updateData });
 487          }
 488          await addTimingNoise();
 489          return { success: false, error: "Invalid signature" };
 490        }
 491        this.challenges.delete(walletKey);
 492  
 493        const connectedAddresses = {};
 494        if (providedAddresses && typeof providedAddresses === "object") {
 495          for (const [chain, address] of Object.entries(providedAddresses)) {
 496            if (address && typeof address === "string" && address.trim().length > 0) {
 497              const chainLower = chain.toLowerCase();
 498              if (chainLower === "ethereum" || chainLower.includes("eth") ||
 499                ["base", "polygon", "arbitrum", "optimism", "avalanche", "fantom", "zksync", "linea", "scroll"].includes(chainLower)) {
 500                connectedAddresses[chain] = address.toLowerCase();
 501              } else {
 502                connectedAddresses[chain] = address;
 503              }
 504            }
 505          }
 506        }
 507  
 508        if (chainType === "solana" && networks.solana?.enabled !== false) {
 509          connectedAddresses.solana = walletKey;
 510        }
 511        if (chainType === "ethereum" && networks.solana?.enabled !== false) {
 512          connectedAddresses.ethereum = walletKey;
 513  
 514          const { getEnabledNetworksForType } = await import("@/data/crypto/networks.js");
 515          const evmNetworks = Object.entries(getEnabledNetworksForType("evm"))
 516            .map(([id, _]) => id);
 517  
 518          evmNetworks.forEach(network => {
 519            if (!connectedAddresses[network]) {
 520              connectedAddresses[network] = walletKey;
 521            }
 522          });
 523        }
 524  
 525        let ipHash;
 526        if (!user) {
 527          const derivationIndex = await hdWalletDeriver.getNextDerivationIndex();
 528          const depositAddresses = hdWalletDeriver.deriveAllAddresses(derivationIndex);
 529          const username = await generateUniqueUsername();
 530          ipHash = hashIpAddress(clientInfo.ipAddress, "temp");
 531  
 532          const { count: ipCount, exceedsLimit } = await checkIpHashAbuse(ipHash);
 533          if (exceedsLimit) {
 534            logger.warn("Too many sessions from same IP hash", { ipHash, count: ipCount });
 535            await addTimingNoise();
 536            return { success: false, error: "Too many active sessions. Please try again later." };
 537          }
 538  
 539          user = await User.create({
 540            walletAddress: walletKey,
 541            walletType,
 542            username,
 543            connectedAddresses,
 544            depositAddresses,
 545            balances: [],
 546            stats: { totalDeposited: 0, totalWithdrawn: 0, depositCount: 0, withdrawalCount: 0 },
 547            security: { failedLoginAttempts: 0 },
 548            withdrawalSettings: { dailyLimit: "100", withdrawnToday: "0", lastWithdrawalReset: new Date() },
 549          });
 550  
 551          const finalIpHash = hashIpAddress(clientInfo.ipAddress, user._id.toString());
 552          await User.updateOne(
 553            { _id: user._id },
 554            { $set: { lastIpHash: finalIpHash } },
 555          );
 556  
 557          logger.info(`New user created: ${walletKey}`, {
 558            ipHash: finalIpHash,
 559            connectedAddresses,
 560            depositAddresses: {
 561              solana: depositAddresses.solana.address,
 562              ethereum: depositAddresses.ethereum.address,
 563              sui: depositAddresses.sui.address,
 564            },
 565          });
 566        } else {
 567          ipHash = hashIpAddress(clientInfo.ipAddress, user._id.toString());
 568          const { count: ipCount, exceedsLimit } = await checkIpHashAbuse(ipHash);
 569          if (exceedsLimit) {
 570            logger.warn("Too many sessions from same IP hash", { ipHash, count: ipCount });
 571            await addTimingNoise();
 572            return { success: false, error: "Too many active sessions. Please try again later." };
 573          }
 574  
 575          const updateData = {
 576            "security.failedLoginAttempts": 0,
 577            lastSeen: new Date(),
 578            lastIpHash: ipHash,
 579          };
 580  
 581          if (user.walletAddress !== walletKey) {
 582            updateData.walletAddress = walletKey;
 583          }
 584          if (!validWalletTypes.includes(user.walletType)) {
 585            updateData.walletType = walletType;
 586          }
 587  
 588          for (const [chain, address] of Object.entries(connectedAddresses)) {
 589            updateData[`connectedAddresses.${chain}`] = address;
 590          }
 591  
 592          if (chainType === "ethereum" && networks.ethereum?.enabled !== false) {
 593            const { getEnabledNetworksForType } = await import("../data/crypto/networks.js");
 594            const evmNetworks = Object.entries(getEnabledNetworksForType("evm"))
 595              .filter(([id, _]) => id !== "ethereum")
 596              .map(([id, _]) => id);
 597  
 598            evmNetworks.forEach(network => {
 599              if (!user.connectedAddresses || !user.connectedAddresses[network]) {
 600                updateData[`connectedAddresses.${network}`] = walletKey;
 601              }
 602            });
 603          }
 604  
 605          await User.updateOne({ _id: user._id }, { $set: updateData });
 606          user.walletAddress = updateData.walletAddress || user.walletAddress;
 607          user.walletType = updateData.walletType || user.walletType;
 608          user.connectedAddresses = { ...(user.connectedAddresses || {}), ...connectedAddresses };
 609        }
 610  
 611        const session = this.sessionManager.createSession(user._id, walletKey);
 612        await User.updateOne(
 613          { _id: user._id },
 614          {
 615            sessionToken: session.sessionToken,
 616            sessionExpiry: session.expiry,
 617            lastSignature: signature,
 618            lastSeen: new Date(),
 619            lastIpHash: ipHash,
 620          },
 621        );
 622  
 623        const { getSupportedCurrencies } = await import("../data/crypto/currencies.js");
 624        const supportedCurrencies = getSupportedCurrencies();
 625        const supportedChains = supportedCurrencies.map(c => c.chain);
 626        const filteredConnectedAddresses = {};
 627        if (user.connectedAddresses) {
 628          for (const chain of supportedChains) {
 629            if (user.connectedAddresses[chain]) {
 630              filteredConnectedAddresses[chain] = user.connectedAddresses[chain];
 631            }
 632          }
 633        }
 634  
 635        const publicDepositAddresses = {};
 636        if (user.depositAddresses) {
 637          for (const [chain, data] of Object.entries(user.depositAddresses)) {
 638            if (data && data.address) {
 639              publicDepositAddresses[chain] = data.address;
 640            }
 641          }
 642        }
 643  
 644        logger.info(`User authenticated: ${walletKey}`, { userId: user._id, ipHash });
 645  
 646        return {
 647          success: true,
 648          user: {
 649            id: user._id.toString(),
 650            username: user.username,
 651            walletAddress: user.walletAddress,
 652            walletType: user.walletType,
 653            balance: centsToDollars(user.fiatBalanceUsd || 0),
 654            stats: user.stats,
 655            connectedAddresses: filteredConnectedAddresses,
 656            depositAddresses: publicDepositAddresses,
 657            hasPassword: !!user.passwordHash,
 658            avatar: user.avatar || null,
 659          },
 660          sessionToken: session.sessionToken,
 661          sessionExpiry: session.expiry,
 662        };
 663      } catch (error) {
 664        logger.error("Authentication error:", error);
 665        return { success: false, error: "Authentication failed. Please try again." };
 666      }
 667    }
 668  
 669    async verifyPassword(pendingSessionToken, password, clientInfo = {}) {
 670      try {
 671        const validation = this.sessionManager.validateSession(pendingSessionToken);
 672        if (!validation.valid) {
 673          const user = await User.findOne({ sessionToken: pendingSessionToken });
 674          if (!user) {
 675            return { success: false, error: "Session not found or expired" };
 676          }
 677          if (user.sessionExpiry && new Date() > user.sessionExpiry) {
 678            return { success: false, error: "Session expired. Please reconnect your wallet." };
 679          }
 680          this.sessionManager.restoreSession(pendingSessionToken, user._id.toString(), user.walletAddress, user.sessionExpiry);
 681          validation = { valid: true, userId: user._id.toString(), walletAddress: user.walletAddress };
 682        }
 683  
 684        const userId = validation.valid ? validation.userId : (await User.findOne({ sessionToken: pendingSessionToken }))?._id;
 685        if (!userId) {
 686          return { success: false, error: "User not found" };
 687        }
 688  
 689        const user = await User.findById(userId);
 690        if (!user) {
 691          return { success: false, error: "User not found" };
 692        }
 693  
 694        if (user.security?.accountLocked) {
 695          if (user.security.accountLockedUntil && new Date() > user.security.accountLockedUntil) {
 696            await User.updateOne(
 697              { _id: user._id },
 698              { $set: { "security.accountLocked": false, "security.failedLoginAttempts": 0 } },
 699            );
 700          } else {
 701            const remainingTime = Math.ceil((user.security.accountLockedUntil - new Date()) / 1000 / 60);
 702            return { success: false, error: `Account is temporarily locked. Try again in ${remainingTime} minutes.` };
 703          }
 704        }
 705  
 706        if (!user.passwordHash || !user.passwordSalt) {
 707          return { success: false, error: "Password not set for this account" };
 708        }
 709  
 710        const crypto = await import("crypto");
 711        const hash = await new Promise((resolve, reject) => {
 712          crypto.pbkdf2(password, user.passwordSalt, 100000, 64, "sha512", (err, derivedKey) => {
 713            if (err) {
 714              reject(err);
 715            } else {
 716              resolve(derivedKey.toString("hex"));
 717            }
 718          });
 719        });
 720  
 721        if (!crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(user.passwordHash))) {
 722          const failedAttempts = (user.security?.failedLoginAttempts || 0) + 1;
 723          const updateData = {
 724            "security.failedLoginAttempts": failedAttempts,
 725            "security.lastFailedLogin": new Date(),
 726          };
 727  
 728          if (failedAttempts >= this.maxFailedLogins) {
 729            updateData["security.accountLocked"] = true;
 730            updateData["security.accountLockedUntil"] = new Date(Date.now() + this.accountLockDuration);
 731            await User.updateOne({ _id: user._id }, { $set: updateData });
 732            return { success: false, error: "Too many failed attempts. Account locked for 30 minutes." };
 733          }
 734  
 735          await User.updateOne({ _id: user._id }, { $set: updateData });
 736          return { success: false, error: `Invalid password. ${this.maxFailedLogins - failedAttempts} attempts remaining.` };
 737        }
 738  
 739        await User.updateOne(
 740          { _id: user._id },
 741          {
 742            $set: {
 743              "security.failedLoginAttempts": 0,
 744              lastSeen: new Date(),
 745              lastIpHash: hashIpAddress(clientInfo.ipAddress, user._id.toString()),
 746            },
 747          },
 748        );
 749  
 750        const { getSupportedCurrencies } = await import("../data/crypto/currencies.js");
 751        const supportedCurrencies = getSupportedCurrencies();
 752        const supportedChains = supportedCurrencies.map(c => c.chain);
 753        const filteredConnectedAddresses = {};
 754        if (user.connectedAddresses) {
 755          for (const chain of supportedChains) {
 756            if (user.connectedAddresses[chain]) {
 757              filteredConnectedAddresses[chain] = user.connectedAddresses[chain];
 758            }
 759          }
 760        }
 761  
 762        const publicDepositAddresses = {};
 763        if (user.depositAddresses) {
 764          for (const [chain, data] of Object.entries(user.depositAddresses)) {
 765            if (data && data.address) {
 766              publicDepositAddresses[chain] = data.address;
 767            }
 768          }
 769        }
 770  
 771        logger.info(`Password verified for user: ${user.walletAddress}`, { userId: user._id, ipHash: hashIpAddress(clientInfo.ipAddress, user._id.toString()) });
 772  
 773        return {
 774          success: true,
 775          user: {
 776            id: user._id.toString(),
 777            username: user.username,
 778            walletAddress: user.walletAddress,
 779            walletType: user.walletType,
 780            balance: centsToDollars(user.fiatBalanceUsd || 0),
 781            stats: user.stats,
 782            connectedAddresses: filteredConnectedAddresses,
 783            depositAddresses: publicDepositAddresses,
 784            hasPassword: true,
 785            avatar: user.avatar || null,
 786          },
 787          sessionToken: pendingSessionToken,
 788          sessionExpiry: user.sessionExpiry,
 789        };
 790      } catch (error) {
 791        logger.error("Password verification error:", error);
 792        return { success: false, error: "Password verification failed. Please try again." };
 793      }
 794    }
 795  
 796    async validateSession(sessionToken) {
 797      let validation = this.sessionManager.validateSession(sessionToken);
 798      if (!validation.valid) {
 799        const user = await User.findOne({ sessionToken });
 800        if (!user) {
 801          return { valid: false, error: "Session not found" };
 802        }
 803        if (user.sessionExpiry && new Date() > user.sessionExpiry) {
 804          return { valid: false, error: "Session expired" };
 805        }
 806        this.sessionManager.restoreSession(sessionToken, user._id.toString(), user.walletAddress, user.sessionExpiry);
 807        validation = { valid: true, userId: user._id.toString(), walletAddress: user.walletAddress };
 808      }
 809  
 810      const user = await User.findById(validation.userId);
 811      if (!user) {
 812        return { valid: false, error: "User not found" };
 813      }
 814  
 815      const { getSupportedCurrencies } = await import("@/data/crypto/currencies.js");
 816      const supportedCurrencies = getSupportedCurrencies();
 817      const supportedChains = supportedCurrencies.map(c => c.chain);
 818      const filteredConnectedAddresses = {};
 819      if (user.connectedAddresses) {
 820        for (const chain of supportedChains) {
 821          if (user.connectedAddresses[chain]) {
 822            filteredConnectedAddresses[chain] = user.connectedAddresses[chain];
 823          }
 824        }
 825      }
 826  
 827      const publicDepositAddresses = {};
 828      if (user.depositAddresses) {
 829        for (const [chain, data] of Object.entries(user.depositAddresses)) {
 830          if (data && data.address) {
 831            publicDepositAddresses[chain] = data.address;
 832          }
 833        }
 834      }
 835  
 836      return {
 837        valid: true,
 838        user: {
 839          id: user._id.toString(),
 840          username: user.username,
 841          walletAddress: user.walletAddress,
 842          walletType: user.walletType,
 843          balance: centsToDollars(user.fiatBalanceUsd || 0),
 844          stats: user.stats,
 845          connectedAddresses: filteredConnectedAddresses,
 846          depositAddresses: publicDepositAddresses,
 847          hasPassword: !!user.passwordHash,
 848          avatar: user.avatar || null,
 849          isBanned: user.isBanned,
 850        },
 851      };
 852    }
 853  
 854    async logout(sessionToken) {
 855      const validation = this.sessionManager.validateSession(sessionToken);
 856      if (validation.valid) {
 857        await User.updateOne({ _id: validation.userId }, { sessionToken: null, sessionExpiry: null });
 858      }
 859  
 860      this.sessionManager.deleteSession(sessionToken);
 861      return { success: true };
 862    }
 863  
 864    cleanupChallenges() {
 865      const now = Date.now();
 866      const expiredChallenges = [];
 867  
 868      for (const [address, challenge] of this.challenges.entries()) {
 869        if (now > challenge.expiry) {
 870          expiredChallenges.push(address);
 871        }
 872      }
 873  
 874      expiredChallenges.forEach(address => this.challenges.delete(address));
 875  
 876      if (this.challenges.size > this.maxChallenges * 0.8) {
 877        const entries = Array.from(this.challenges.entries());
 878        const oldestEntries = entries
 879          .sort((a, b) => a[1].timestamp - b[1].timestamp)
 880          .slice(0, Math.floor(this.maxChallenges * 0.2));
 881        oldestEntries.forEach(([key]) => this.challenges.delete(key));
 882      }
 883    }
 884  }
 885  
 886  export const authService = new AuthService();
 887  export { sessionManager };
 888  
 889  setInterval(() => {
 890    authService.cleanupChallenges();
 891  }, 60 * 1000);
 892  
 893  const balanceCache = new Map();
 894  const transactionCache = new Map();
 895  const MAX_CACHE_SIZE = 1000;
 896  
 897  const cleanupCache = (cache, maxSize) => {
 898    if (cache.size > maxSize) {
 899      const entriesToDelete = cache.size - maxSize;
 900      const keys = Array.from(cache.keys()).slice(0, entriesToDelete);
 901      keys.forEach(key => cache.delete(key));
 902    }
 903  };
 904  
 905  const getRpcClients = () => {
 906    const solanaRpc = config.rpc.solana.mainnet;
 907    const ethereumRpc = config.rpc.ethereum.mainnet;
 908    const suiRpc = config.rpc.sui.mainnet;
 909  
 910    return {
 911      solana: new Connection(solanaRpc, { commitment: "confirmed", confirmTransactionInitialTimeout: config.timeouts.long }),
 912      ethereum: new ethers.JsonRpcProvider(ethereumRpc, undefined, { staticNetwork: true }),
 913      sui: new SuiClient({ url: suiRpc }),
 914    };
 915  };
 916  
 917  const rpcClients = getRpcClients();
 918  
 919  setInterval(() => {
 920    cleanupCache(balanceCache, MAX_CACHE_SIZE);
 921    cleanupCache(transactionCache, MAX_CACHE_SIZE);
 922  }, config.timeouts.veryLong);
 923  
 924  export const getWalletBalance = async (address, chain) => {
 925    try {
 926      const cacheKey = `${address}:${chain}`;
 927      const cached = balanceCache.get(cacheKey);
 928      if (cached && Date.now() - cached.timestamp < config.timeouts.long) {
 929        return cached.data;
 930      }
 931  
 932      let balanceData;
 933      if (chain === "solana") {
 934        const pubkey = new PublicKey(address);
 935        const balance = await rpcClients.solana.getBalance(pubkey);
 936        const balanceInSol = balance / LAMPORTS_PER_SOL;
 937        balanceData = { chain, address, balance: balance.toString(), denominatedBalance: balanceInSol.toString(), decimals: 9 };
 938      } else if (chain === "sui") {
 939        const coins = await rpcClients.sui.getCoins({ owner: address, coinType: "0x2::sui::SUI" });
 940        let totalBalance = BigInt(0);
 941  
 942        for (const coin of coins.data) {
 943          totalBalance += BigInt(coin.balance);
 944        }
 945  
 946        const balanceInSui = Number(totalBalance) / 1e9;
 947        balanceData = { chain, address, balance: totalBalance.toString(), denominatedBalance: balanceInSui.toString(), decimals: 9 };
 948      } else if (networks[chain]?.type === "evm" && networks[chain]?.enabled !== false) {
 949        const balance = await rpcClients.ethereum.getBalance(address);
 950        const balanceInEth = ethers.formatEther(balance);
 951        balanceData = { chain, address, balance: balance.toString(), denominatedBalance: balanceInEth, decimals: 18 };
 952      } else {
 953        throw new Error(`Unsupported chain: ${chain}`);
 954      }
 955  
 956      balanceCache.set(cacheKey, { timestamp: Date.now(), data: balanceData });
 957      cleanupCache(balanceCache, MAX_CACHE_SIZE);
 958      return balanceData;
 959    } catch (error) {
 960      logger.error(`Error fetching wallet balance for ${address} on ${chain}:`, error);
 961      throw error;
 962    }
 963  };
 964  
 965  export const getTransactionHistory = async (address, chain, options = {}) => {
 966    try {
 967      const { sort = "DESC", limit = 50 } = options;
 968      const cacheKey = `${address}:${chain}:${sort}:${limit}`;
 969      const cached = transactionCache.get(cacheKey);
 970      if (cached && Date.now() - cached.timestamp < 2 * (config.timeouts.long || 30000)) {
 971        return cached.data;
 972      }
 973  
 974      let transactions = [];
 975      if (chain === "solana" && networks.solana?.enabled !== false) {
 976        const pubkey = new PublicKey(address);
 977        const signatures = await rpcClients.solana.getSignaturesForAddress(pubkey, { limit: Math.min(limit, 100) });
 978        transactions = signatures.map(sig => ({
 979          hash: sig.signature,
 980          blockNumber: sig.slot,
 981          timestamp: sig.blockTime ? sig.blockTime * 1000 : null,
 982          status: sig.err ? "failed" : "confirmed",
 983        }));
 984      } else if (chain === "ethereum" && networks.ethereum?.enabled !== false) {
 985        const currentBlock = await rpcClients.ethereum.getBlockNumber();
 986        const txs = [];
 987        for (let i = 0; i < Math.min(10, limit); i++) {
 988          try {
 989            const block = await rpcClients.ethereum.getBlock(currentBlock - i, true);
 990            if (block && block.transactions) {
 991              for (const tx of block.transactions) {
 992                if (tx.from?.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase()) {
 993                  txs.push({
 994                    hash: tx.hash,
 995                    blockNumber: block.number,
 996                    timestamp: block.timestamp * 1000,
 997                    from: tx.from,
 998                    to: tx.to,
 999                    value: ethers.formatEther(tx.value),
1000                    status: "confirmed",
1001                  });
1002                }
1003              }
1004            }
1005          } catch {
1006            break;
1007          }
1008        }
1009        transactions = txs;
1010      } else if (chain === "sui" && networks.sui?.enabled !== false) {
1011        const result = await rpcClients.sui.queryTransactionBlocks({
1012          filter: { FromAddress: address },
1013          options: { showEffects: true },
1014          limit: Math.min(limit, 50),
1015        });
1016        transactions = result.data.map(tx => ({
1017          hash: tx.digest,
1018          blockNumber: tx.checkpoint ? parseInt(tx.checkpoint) : 0,
1019          timestamp: tx.timestampMs ? parseInt(tx.timestampMs) : null,
1020          status: tx.effects?.status?.status === "success" ? "confirmed" : "failed",
1021        }));
1022      } else {
1023        throw new Error(`Unsupported chain: ${chain}`);
1024      }
1025  
1026      if (sort === "ASC") {
1027        transactions = transactions.sort((a, b) => a.blockNumber - b.blockNumber);
1028      }
1029  
1030      const transactionData = { transactions, prevPage: "", nextPage: "" };
1031      transactionCache.set(cacheKey, { timestamp: Date.now(), data: transactionData });
1032      cleanupCache(transactionCache, MAX_CACHE_SIZE);
1033      return transactionData;
1034    } catch (error) {
1035      logger.error(`Error fetching transaction history for ${address} on ${chain}:`, error);
1036      throw error;
1037    }
1038  };
1039  
1040  export const findOrCreateUser = async (username) => {
1041    try {
1042      let user = await User.findOne({ username });
1043      if (!user) {
1044        user = await User.create({
1045          username,
1046          stats: { totalDeposited: 0, totalWithdrawn: 0 },
1047        });
1048        logger.info(`Created new user: ${username}`);
1049      } else {
1050        user.lastSeen = Date.now();
1051        await user.save();
1052      }
1053  
1054      return user;
1055    } catch (error) {
1056      logger.error(error);
1057      throw error;
1058    }
1059  };
1060  
1061  export const depositCoins = async (userId, amount, currency, chain, priceAtTransaction) => {
1062    try {
1063      const user = await User.findById(userId);
1064      if (!user) {
1065        throw new Error("User not found");
1066      }
1067  
1068      const cryptoAmount = parseFloat(amount);
1069      const usdValue = cryptoAmount * priceAtTransaction;
1070      const currentCents = user.fiatBalanceUsd || 0;
1071      const addCents = dollarsToCents(usdValue);
1072      user.fiatBalanceUsd = currentCents + addCents;
1073  
1074      user.stats.totalDeposited += cryptoAmount;
1075      await user.save();
1076      return { success: true, newBalance: centsToDollars(user.fiatBalanceUsd) };
1077    } catch (error) {
1078      logger.error("Error depositing coins:", error);
1079      throw error;
1080    }
1081  };
1082  
1083  export const deductCoins = async (userId, amount, currency, currentPrice) => {
1084    try {
1085      const user = await User.findById(userId);
1086      if (!user) {
1087        throw new Error("User not found");
1088      }
1089  
1090      const cryptoAmount = parseFloat(amount);
1091      const usdValue = cryptoAmount * currentPrice;
1092      const currentCents = user.fiatBalanceUsd || 0;
1093      const deductCents = dollarsToCents(usdValue);
1094  
1095      if (currentCents < deductCents) {
1096        throw new Error("Insufficient balance");
1097      }
1098  
1099      user.fiatBalanceUsd = currentCents - deductCents;
1100      user.stats.totalWithdrawn += cryptoAmount;
1101      await user.save();
1102      return { success: true, newBalance: centsToDollars(user.fiatBalanceUsd) };
1103    } catch (error) {
1104      logger.error("Error withdrawing coins:", error);
1105      throw error;
1106    }
1107  };
1108  
1109  export const banUser = async (userId, reason = "Violation of terms") => {
1110    try {
1111      const user = await User.findById(userId);
1112      if (!user) {
1113        throw new Error("User not found");
1114      }
1115  
1116      if (user.isBanned) {
1117        return { success: false, error: "User is already banned" };
1118      }
1119  
1120      user.isBanned = true;
1121      user.bannedAt = new Date();
1122      user.banReason = reason;
1123      await user.save();
1124  
1125      cache.emit(CacheEvents.USER_BANNED, {
1126        userId: userId.toString(),
1127        reason,
1128        timestamp: new Date(),
1129        clearAll: true,
1130      });
1131  
1132      logger.info(`User ${userId} banned: ${reason}`);
1133      return { success: true, message: "User banned successfully" };
1134    } catch (error) {
1135      logger.error("Error banning user:", error);
1136      throw error;
1137    }
1138  };
1139  
1140  export const unbanUser = async (userId) => {
1141    try {
1142      const user = await User.findById(userId);
1143      if (!user) {
1144        throw new Error("User not found");
1145      }
1146  
1147      if (!user.isBanned) {
1148        return { success: false, error: "User is not banned" };
1149      }
1150  
1151      user.isBanned = false;
1152      user.bannedAt = null;
1153      user.banReason = null;
1154      await user.save();
1155  
1156      cache.emit(CacheEvents.USER_UNBANNED, {
1157        userId: userId.toString(),
1158        timestamp: new Date(),
1159      });
1160  
1161      logger.info(`User ${userId} unbanned`);
1162      return { success: true, message: "User unbanned successfully" };
1163    } catch (error) {
1164      logger.error("Error unbanning user:", error);
1165      throw error;
1166    }
1167  };
1168  
1169  export const invalidateUserSessions = async (userId, reason = "Security action") => {
1170    try {
1171      cache.emit(CacheEvents.SESSION_INVALIDATED, {
1172        userId: userId.toString(),
1173        reason,
1174        timestamp: new Date(),
1175      });
1176  
1177      logger.info(`All sessions invalidated for user ${userId}: ${reason}`);
1178      return { success: true, message: "All sessions invalidated" };
1179    } catch (error) {
1180      logger.error("Error invalidating sessions:", error);
1181      throw error;
1182    }
1183  };