diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 3e049bc..1a3a0b3 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -108,11 +108,17 @@ router.post( }); } - // Create session + // Delete old sessions and create new one + await prisma.session.deleteMany({ where: { userId: user.id } }); + const sessionToken = jwt.sign( + { userId: user.id, nonce: Date.now() }, + JWT_SECRET, + { expiresIn: "7d" }, + ); const session = await prisma.session.create({ data: { userId: user.id, - token: jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "7d" }), + token: sessionToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, }); diff --git a/backend/src/services/solana.ts b/backend/src/services/solana.ts index a5fbea2..eec347e 100644 --- a/backend/src/services/solana.ts +++ b/backend/src/services/solana.ts @@ -1,220 +1,232 @@ import { - Connection, - PublicKey, - Keypair, - Transaction, - SystemProgram, - sendAndConfirmTransaction -} from '@solana/web3.js'; + ASSOCIATED_TOKEN_PROGRAM_ID, + createBurnInstruction, + createMintToInstruction, + getAccount, + getOrCreateAssociatedTokenAccount, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; import { - getOrCreateAssociatedTokenAccount, - createMintToInstruction, - createBurnInstruction, - getAccount, - TOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID -} from '@solana/spl-token'; -import bs58 from 'bs58'; -import { prisma } from '../utils/prisma'; + Connection, + Keypair, + PublicKey, + SystemProgram, + sendAndConfirmTransaction, + Transaction, +} from "@solana/web3.js"; +import bs58 from "bs58"; +import { prisma } from "../utils/prisma"; -const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com'; -const PROGRAM_ID = new PublicKey(process.env.SOLANA_PROGRAM_ID || 'CoopCredits111111111111111111111111111111111'); -const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || '6'); +const RPC_URL = process.env.SOLANA_RPC_URL || "https://api.devnet.solana.com"; +const PROGRAM_ID = new PublicKey( + process.env.SOLANA_PROGRAM_ID || + "CoopCredits111111111111111111111111111111111", +); +const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || "6"); // Backend mint authority keypair (stored securely) let mintAuthority: Keypair | null = null; try { - if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) { - const secretKey = bs58.decode(process.env.SOLANA_MINT_AUTHORITY_KEYPAIR); - mintAuthority = Keypair.fromSecretKey(secretKey); - } + if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) { + const raw = process.env.SOLANA_MINT_AUTHORITY_KEYPAIR.trim(); + let secretKey: Uint8Array; + if (raw.startsWith("[")) { + secretKey = new Uint8Array(JSON.parse(raw)); + } else { + secretKey = bs58.decode(raw); + } + mintAuthority = Keypair.fromSecretKey(secretKey); + } } catch (error) { - console.warn('Mint authority not configured'); + console.warn("Mint authority not configured"); } -export const connection = new Connection(RPC_URL, 'confirmed'); +export const connection = new Connection(RPC_URL, "confirmed"); // Get token mint address from program state export async function getTokenMint(): Promise { - try { - // In a real implementation, you'd derive this from the program state - // For now, return from environment or stored config - const config = await prisma.systemSettings.findFirst(); - if (config) { - // Store/retrieve mint address from config - return null; // Placeholder - } - return null; - } catch (error) { - console.error('Failed to get token mint:', error); - return null; - } + try { + // In a real implementation, you'd derive this from the program state + // For now, return from environment or stored config + const config = await prisma.systemSettings.findFirst(); + if (config) { + // Store/retrieve mint address from config + return null; // Placeholder + } + return null; + } catch (error) { + console.error("Failed to get token mint:", error); + return null; + } } // Create a new Solana wallet for a user export function createWallet(): { publicKey: string; secretKey: string } { - const keypair = Keypair.generate(); - - return { - publicKey: keypair.publicKey.toBase58(), - secretKey: bs58.encode(keypair.secretKey) - }; + const keypair = Keypair.generate(); + + return { + publicKey: keypair.publicKey.toBase58(), + secretKey: bs58.encode(keypair.secretKey), + }; } // Get or create token account for user export async function getOrCreateTokenAccount( - userPublicKey: PublicKey, - mint: PublicKey + userPublicKey: PublicKey, + mint: PublicKey, ): Promise { - const tokenAccount = await getOrCreateAssociatedTokenAccount( - connection, - mintAuthority!, // payer - mint, - userPublicKey - ); - - return tokenAccount.address; + const tokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + mintAuthority!, // payer + mint, + userPublicKey, + ); + + return tokenAccount.address; } // Mint tokens to user (called by backend after watch event) export async function mintTokens( - userWalletAddress: string, - amount: number, - metadata: { - sessionId: string; - contentTitle: string; - watchDurationMinutes: number; - } + userWalletAddress: string, + amount: number, + metadata: { + sessionId: string; + contentTitle: string; + watchDurationMinutes: number; + }, ): Promise { - if (!mintAuthority) { - throw new Error('Mint authority not configured'); - } + if (!mintAuthority) { + throw new Error("Mint authority not configured"); + } - try { - const userPublicKey = new PublicKey(userWalletAddress); - const mint = await getTokenMint(); - - if (!mint) { - throw new Error('Token mint not found'); - } + try { + const userPublicKey = new PublicKey(userWalletAddress); + const mint = await getTokenMint(); - // Get or create user's token account - const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint); - - // Calculate amount with decimals - const amountWithDecimals = amount * Math.pow(10, DECIMALS); - - // Create mint instruction - const mintInstruction = createMintToInstruction( - mint, - tokenAccount, - mintAuthority.publicKey, - BigInt(Math.floor(amountWithDecimals)) - ); - - // Create and send transaction - const transaction = new Transaction().add(mintInstruction); - const signature = await sendAndConfirmTransaction( - connection, - transaction, - [mintAuthority] - ); - - console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`); - - return signature; - } catch (error) { - console.error('Failed to mint tokens:', error); - return null; - } + if (!mint) { + throw new Error("Token mint not found"); + } + + // Get or create user's token account + const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint); + + // Calculate amount with decimals + const amountWithDecimals = amount * 10 ** DECIMALS; + + // Create mint instruction + const mintInstruction = createMintToInstruction( + mint, + tokenAccount, + mintAuthority.publicKey, + BigInt(Math.floor(amountWithDecimals)), + ); + + // Create and send transaction + const transaction = new Transaction().add(mintInstruction); + const signature = await sendAndConfirmTransaction(connection, transaction, [ + mintAuthority, + ]); + + console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`); + + return signature; + } catch (error) { + console.error("Failed to mint tokens:", error); + return null; + } } // Burn tokens from user (called when content request is approved) export async function burnTokens( - userWalletAddress: string, - userSecretKey: string, - amount: number + userWalletAddress: string, + userSecretKey: string, + amount: number, ): Promise { - try { - const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey)); - const mint = await getTokenMint(); - - if (!mint) { - throw new Error('Token mint not found'); - } + try { + const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey)); + const mint = await getTokenMint(); - const userPublicKey = new PublicKey(userWalletAddress); - const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint); - - // Check balance - const accountInfo = await getAccount(connection, tokenAccount); - const amountWithDecimals = BigInt(Math.floor(amount * Math.pow(10, DECIMALS))); - - if (accountInfo.amount < amountWithDecimals) { - throw new Error('Insufficient balance'); - } - - // Create burn instruction - const burnInstruction = createBurnInstruction( - tokenAccount, - mint, - userKeypair.publicKey, - amountWithDecimals - ); - - const transaction = new Transaction().add(burnInstruction); - const signature = await sendAndConfirmTransaction( - connection, - transaction, - [userKeypair] - ); - - console.log(`Burned ${amount} COOP from ${userWalletAddress}: ${signature}`); - - return signature; - } catch (error) { - console.error('Failed to burn tokens:', error); - return null; - } + if (!mint) { + throw new Error("Token mint not found"); + } + + const userPublicKey = new PublicKey(userWalletAddress); + const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint); + + // Check balance + const accountInfo = await getAccount(connection, tokenAccount); + const amountWithDecimals = BigInt(Math.floor(amount * 10 ** DECIMALS)); + + if (accountInfo.amount < amountWithDecimals) { + throw new Error("Insufficient balance"); + } + + // Create burn instruction + const burnInstruction = createBurnInstruction( + tokenAccount, + mint, + userKeypair.publicKey, + amountWithDecimals, + ); + + const transaction = new Transaction().add(burnInstruction); + const signature = await sendAndConfirmTransaction(connection, transaction, [ + userKeypair, + ]); + + console.log( + `Burned ${amount} COOP from ${userWalletAddress}: ${signature}`, + ); + + return signature; + } catch (error) { + console.error("Failed to burn tokens:", error); + return null; + } } // Get token balance for user export async function getTokenBalance(walletAddress: string): Promise { - try { - const mint = await getTokenMint(); - if (!mint) return 0; + try { + const mint = await getTokenMint(); + if (!mint) return 0; - const userPublicKey = new PublicKey(walletAddress); - - try { - const tokenAccount = await getOrCreateAssociatedTokenAccount( - connection, - mintAuthority!, - mint, - userPublicKey - ); - - const accountInfo = await getAccount(connection, tokenAccount.address); - return Number(accountInfo.amount) / Math.pow(10, DECIMALS); - } catch { - return 0; - } - } catch (error) { - console.error('Failed to get token balance:', error); - return 0; - } + const userPublicKey = new PublicKey(walletAddress); + + try { + const tokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + mintAuthority!, + mint, + userPublicKey, + ); + + const accountInfo = await getAccount(connection, tokenAccount.address); + return Number(accountInfo.amount) / 10 ** DECIMALS; + } catch { + return 0; + } + } catch (error) { + console.error("Failed to get token balance:", error); + return 0; + } } // Request airdrop for testing (devnet only) -export async function requestAirdrop(walletAddress: string): Promise { - try { - const publicKey = new PublicKey(walletAddress); - const signature = await connection.requestAirdrop(publicKey, 2 * 1000000000); // 2 SOL - await connection.confirmTransaction(signature); - return signature; - } catch (error) { - console.error('Airdrop failed:', error); - return null; - } +export async function requestAirdrop( + walletAddress: string, +): Promise { + try { + const publicKey = new PublicKey(walletAddress); + const signature = await connection.requestAirdrop( + publicKey, + 2 * 1000000000, + ); // 2 SOL + await connection.confirmTransaction(signature); + return signature; + } catch (error) { + console.error("Airdrop failed:", error); + return null; + } }