fix(backend): session dedupe + mint authority JSON array parsing
1. Auth callback now deletes old sessions before creating new one, and adds Date.now() nonce to JWT to prevent unique constraint violations on duplicate login attempts. 2. Solana mint authority now accepts both JSON array and base58 formats for SOLANA_MINT_AUTHORITY_KEYPAIR env var.
This commit is contained in:
@@ -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({
|
const session = await prisma.session.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
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),
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+187
-175
@@ -1,220 +1,232 @@
|
|||||||
import {
|
import {
|
||||||
Connection,
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
PublicKey,
|
createBurnInstruction,
|
||||||
Keypair,
|
createMintToInstruction,
|
||||||
Transaction,
|
getAccount,
|
||||||
SystemProgram,
|
getOrCreateAssociatedTokenAccount,
|
||||||
sendAndConfirmTransaction
|
TOKEN_PROGRAM_ID,
|
||||||
} from '@solana/web3.js';
|
} from "@solana/spl-token";
|
||||||
import {
|
import {
|
||||||
getOrCreateAssociatedTokenAccount,
|
Connection,
|
||||||
createMintToInstruction,
|
Keypair,
|
||||||
createBurnInstruction,
|
PublicKey,
|
||||||
getAccount,
|
SystemProgram,
|
||||||
TOKEN_PROGRAM_ID,
|
sendAndConfirmTransaction,
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
Transaction,
|
||||||
} from '@solana/spl-token';
|
} from "@solana/web3.js";
|
||||||
import bs58 from 'bs58';
|
import bs58 from "bs58";
|
||||||
import { prisma } from '../utils/prisma';
|
import { prisma } from "../utils/prisma";
|
||||||
|
|
||||||
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com';
|
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 PROGRAM_ID = new PublicKey(
|
||||||
const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || '6');
|
process.env.SOLANA_PROGRAM_ID ||
|
||||||
|
"CoopCredits111111111111111111111111111111111",
|
||||||
|
);
|
||||||
|
const DECIMALS = parseInt(process.env.SOLANA_TOKEN_DECIMALS || "6");
|
||||||
|
|
||||||
// Backend mint authority keypair (stored securely)
|
// Backend mint authority keypair (stored securely)
|
||||||
let mintAuthority: Keypair | null = null;
|
let mintAuthority: Keypair | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) {
|
if (process.env.SOLANA_MINT_AUTHORITY_KEYPAIR) {
|
||||||
const secretKey = bs58.decode(process.env.SOLANA_MINT_AUTHORITY_KEYPAIR);
|
const raw = process.env.SOLANA_MINT_AUTHORITY_KEYPAIR.trim();
|
||||||
mintAuthority = Keypair.fromSecretKey(secretKey);
|
let secretKey: Uint8Array;
|
||||||
}
|
if (raw.startsWith("[")) {
|
||||||
|
secretKey = new Uint8Array(JSON.parse(raw));
|
||||||
|
} else {
|
||||||
|
secretKey = bs58.decode(raw);
|
||||||
|
}
|
||||||
|
mintAuthority = Keypair.fromSecretKey(secretKey);
|
||||||
|
}
|
||||||
} catch (error) {
|
} 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
|
// Get token mint address from program state
|
||||||
export async function getTokenMint(): Promise<PublicKey | null> {
|
export async function getTokenMint(): Promise<PublicKey | null> {
|
||||||
try {
|
try {
|
||||||
// In a real implementation, you'd derive this from the program state
|
// In a real implementation, you'd derive this from the program state
|
||||||
// For now, return from environment or stored config
|
// For now, return from environment or stored config
|
||||||
const config = await prisma.systemSettings.findFirst();
|
const config = await prisma.systemSettings.findFirst();
|
||||||
if (config) {
|
if (config) {
|
||||||
// Store/retrieve mint address from config
|
// Store/retrieve mint address from config
|
||||||
return null; // Placeholder
|
return null; // Placeholder
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get token mint:', error);
|
console.error("Failed to get token mint:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new Solana wallet for a user
|
// Create a new Solana wallet for a user
|
||||||
export function createWallet(): { publicKey: string; secretKey: string } {
|
export function createWallet(): { publicKey: string; secretKey: string } {
|
||||||
const keypair = Keypair.generate();
|
const keypair = Keypair.generate();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicKey: keypair.publicKey.toBase58(),
|
publicKey: keypair.publicKey.toBase58(),
|
||||||
secretKey: bs58.encode(keypair.secretKey)
|
secretKey: bs58.encode(keypair.secretKey),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create token account for user
|
// Get or create token account for user
|
||||||
export async function getOrCreateTokenAccount(
|
export async function getOrCreateTokenAccount(
|
||||||
userPublicKey: PublicKey,
|
userPublicKey: PublicKey,
|
||||||
mint: PublicKey
|
mint: PublicKey,
|
||||||
): Promise<PublicKey> {
|
): Promise<PublicKey> {
|
||||||
const tokenAccount = await getOrCreateAssociatedTokenAccount(
|
const tokenAccount = await getOrCreateAssociatedTokenAccount(
|
||||||
connection,
|
connection,
|
||||||
mintAuthority!, // payer
|
mintAuthority!, // payer
|
||||||
mint,
|
mint,
|
||||||
userPublicKey
|
userPublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
return tokenAccount.address;
|
return tokenAccount.address;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mint tokens to user (called by backend after watch event)
|
// Mint tokens to user (called by backend after watch event)
|
||||||
export async function mintTokens(
|
export async function mintTokens(
|
||||||
userWalletAddress: string,
|
userWalletAddress: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
metadata: {
|
metadata: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
contentTitle: string;
|
contentTitle: string;
|
||||||
watchDurationMinutes: number;
|
watchDurationMinutes: number;
|
||||||
}
|
},
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
if (!mintAuthority) {
|
if (!mintAuthority) {
|
||||||
throw new Error('Mint authority not configured');
|
throw new Error("Mint authority not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userPublicKey = new PublicKey(userWalletAddress);
|
const userPublicKey = new PublicKey(userWalletAddress);
|
||||||
const mint = await getTokenMint();
|
const mint = await getTokenMint();
|
||||||
|
|
||||||
if (!mint) {
|
|
||||||
throw new Error('Token mint not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create user's token account
|
if (!mint) {
|
||||||
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
|
throw new Error("Token mint not found");
|
||||||
|
}
|
||||||
// Calculate amount with decimals
|
|
||||||
const amountWithDecimals = amount * Math.pow(10, DECIMALS);
|
// Get or create user's token account
|
||||||
|
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
|
||||||
// Create mint instruction
|
|
||||||
const mintInstruction = createMintToInstruction(
|
// Calculate amount with decimals
|
||||||
mint,
|
const amountWithDecimals = amount * 10 ** DECIMALS;
|
||||||
tokenAccount,
|
|
||||||
mintAuthority.publicKey,
|
// Create mint instruction
|
||||||
BigInt(Math.floor(amountWithDecimals))
|
const mintInstruction = createMintToInstruction(
|
||||||
);
|
mint,
|
||||||
|
tokenAccount,
|
||||||
// Create and send transaction
|
mintAuthority.publicKey,
|
||||||
const transaction = new Transaction().add(mintInstruction);
|
BigInt(Math.floor(amountWithDecimals)),
|
||||||
const signature = await sendAndConfirmTransaction(
|
);
|
||||||
connection,
|
|
||||||
transaction,
|
// Create and send transaction
|
||||||
[mintAuthority]
|
const transaction = new Transaction().add(mintInstruction);
|
||||||
);
|
const signature = await sendAndConfirmTransaction(connection, transaction, [
|
||||||
|
mintAuthority,
|
||||||
console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`);
|
]);
|
||||||
|
|
||||||
return signature;
|
console.log(`Minted ${amount} COOP to ${userWalletAddress}: ${signature}`);
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to mint tokens:', error);
|
return signature;
|
||||||
return null;
|
} catch (error) {
|
||||||
}
|
console.error("Failed to mint tokens:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Burn tokens from user (called when content request is approved)
|
// Burn tokens from user (called when content request is approved)
|
||||||
export async function burnTokens(
|
export async function burnTokens(
|
||||||
userWalletAddress: string,
|
userWalletAddress: string,
|
||||||
userSecretKey: string,
|
userSecretKey: string,
|
||||||
amount: number
|
amount: number,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey));
|
const userKeypair = Keypair.fromSecretKey(bs58.decode(userSecretKey));
|
||||||
const mint = await getTokenMint();
|
const mint = await getTokenMint();
|
||||||
|
|
||||||
if (!mint) {
|
|
||||||
throw new Error('Token mint not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userPublicKey = new PublicKey(userWalletAddress);
|
if (!mint) {
|
||||||
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
|
throw new Error("Token mint not found");
|
||||||
|
}
|
||||||
// Check balance
|
|
||||||
const accountInfo = await getAccount(connection, tokenAccount);
|
const userPublicKey = new PublicKey(userWalletAddress);
|
||||||
const amountWithDecimals = BigInt(Math.floor(amount * Math.pow(10, DECIMALS)));
|
const tokenAccount = await getOrCreateTokenAccount(userPublicKey, mint);
|
||||||
|
|
||||||
if (accountInfo.amount < amountWithDecimals) {
|
// Check balance
|
||||||
throw new Error('Insufficient balance');
|
const accountInfo = await getAccount(connection, tokenAccount);
|
||||||
}
|
const amountWithDecimals = BigInt(Math.floor(amount * 10 ** DECIMALS));
|
||||||
|
|
||||||
// Create burn instruction
|
if (accountInfo.amount < amountWithDecimals) {
|
||||||
const burnInstruction = createBurnInstruction(
|
throw new Error("Insufficient balance");
|
||||||
tokenAccount,
|
}
|
||||||
mint,
|
|
||||||
userKeypair.publicKey,
|
// Create burn instruction
|
||||||
amountWithDecimals
|
const burnInstruction = createBurnInstruction(
|
||||||
);
|
tokenAccount,
|
||||||
|
mint,
|
||||||
const transaction = new Transaction().add(burnInstruction);
|
userKeypair.publicKey,
|
||||||
const signature = await sendAndConfirmTransaction(
|
amountWithDecimals,
|
||||||
connection,
|
);
|
||||||
transaction,
|
|
||||||
[userKeypair]
|
const transaction = new Transaction().add(burnInstruction);
|
||||||
);
|
const signature = await sendAndConfirmTransaction(connection, transaction, [
|
||||||
|
userKeypair,
|
||||||
console.log(`Burned ${amount} COOP from ${userWalletAddress}: ${signature}`);
|
]);
|
||||||
|
|
||||||
return signature;
|
console.log(
|
||||||
} catch (error) {
|
`Burned ${amount} COOP from ${userWalletAddress}: ${signature}`,
|
||||||
console.error('Failed to burn tokens:', error);
|
);
|
||||||
return null;
|
|
||||||
}
|
return signature;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to burn tokens:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get token balance for user
|
// Get token balance for user
|
||||||
export async function getTokenBalance(walletAddress: string): Promise<number> {
|
export async function getTokenBalance(walletAddress: string): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const mint = await getTokenMint();
|
const mint = await getTokenMint();
|
||||||
if (!mint) return 0;
|
if (!mint) return 0;
|
||||||
|
|
||||||
const userPublicKey = new PublicKey(walletAddress);
|
const userPublicKey = new PublicKey(walletAddress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenAccount = await getOrCreateAssociatedTokenAccount(
|
const tokenAccount = await getOrCreateAssociatedTokenAccount(
|
||||||
connection,
|
connection,
|
||||||
mintAuthority!,
|
mintAuthority!,
|
||||||
mint,
|
mint,
|
||||||
userPublicKey
|
userPublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
const accountInfo = await getAccount(connection, tokenAccount.address);
|
const accountInfo = await getAccount(connection, tokenAccount.address);
|
||||||
return Number(accountInfo.amount) / Math.pow(10, DECIMALS);
|
return Number(accountInfo.amount) / 10 ** DECIMALS;
|
||||||
} catch {
|
} catch {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get token balance:', error);
|
console.error("Failed to get token balance:", error);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request airdrop for testing (devnet only)
|
// Request airdrop for testing (devnet only)
|
||||||
export async function requestAirdrop(walletAddress: string): Promise<string | null> {
|
export async function requestAirdrop(
|
||||||
try {
|
walletAddress: string,
|
||||||
const publicKey = new PublicKey(walletAddress);
|
): Promise<string | null> {
|
||||||
const signature = await connection.requestAirdrop(publicKey, 2 * 1000000000); // 2 SOL
|
try {
|
||||||
await connection.confirmTransaction(signature);
|
const publicKey = new PublicKey(walletAddress);
|
||||||
return signature;
|
const signature = await connection.requestAirdrop(
|
||||||
} catch (error) {
|
publicKey,
|
||||||
console.error('Airdrop failed:', error);
|
2 * 1000000000,
|
||||||
return null;
|
); // 2 SOL
|
||||||
}
|
await connection.confirmTransaction(signature);
|
||||||
|
return signature;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Airdrop failed:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user