import { ASSOCIATED_TOKEN_PROGRAM_ID, createBurnInstruction, createMintToInstruction, getAccount, getOrCreateAssociatedTokenAccount, TOKEN_PROGRAM_ID, } from "@solana/spl-token"; import { 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"); // Backend mint authority keypair (stored securely) let mintAuthority: Keypair | null = null; try { 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"); } 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; } } // 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), }; } // Get or create token account for user export async function getOrCreateTokenAccount( userPublicKey: PublicKey, mint: PublicKey, ): Promise { 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; }, ): Promise { 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"); } // 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, ): Promise { try { const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey)); const mint = await getTokenMint(); 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; 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; } }