feat: add CoopCredits Solana media rewards ecosystem
Add complete token system with Plex/Tautulli/Overseer integration: - Anchor program for SPL token mint/burn/transfer - Express backend with OAuth, webhooks, Solana integration - Next.js frontend with dashboard, admin panel, wallet management - Docker deployment for 172.20.1.0/24 infrastructure - Production configs with SSL, Nginx, health monitoring Tautulli webhooks auto-mint on watch events. Overseer integration burns for content requests.
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate, AuthenticatedRequest, requireAdmin } from '../middleware/auth';
|
||||
import { prisma } from '../utils/prisma';
|
||||
import { createWallet, getTokenBalance, requestAirdrop } from '../services/solana';
|
||||
import { asyncHandler } from '../middleware/errorHandler';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const router = Router();
|
||||
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'default-key-32-chars-long!!!!!';
|
||||
|
||||
// Encrypt private key
|
||||
function encrypt(text: string): string {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY), iv);
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
const authTag = cipher.getAuthTag();
|
||||
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
|
||||
}
|
||||
|
||||
// Decrypt private key
|
||||
function decrypt(encryptedData: string): string {
|
||||
const parts = encryptedData.split(':');
|
||||
const iv = Buffer.from(parts[0], 'hex');
|
||||
const authTag = Buffer.from(parts[1], 'hex');
|
||||
const encrypted = parts[2];
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY), iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// Get user's wallet info
|
||||
router.get('/',
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id },
|
||||
select: {
|
||||
walletAddress: true,
|
||||
totalEarned: true,
|
||||
totalSpent: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user?.walletAddress) {
|
||||
return res.json({
|
||||
hasWallet: false,
|
||||
balance: 0,
|
||||
totalEarned: user?.totalEarned || 0,
|
||||
totalSpent: user?.totalSpent || 0
|
||||
});
|
||||
}
|
||||
|
||||
// Get on-chain balance
|
||||
const balance = await getTokenBalance(user.walletAddress);
|
||||
|
||||
res.json({
|
||||
hasWallet: true,
|
||||
address: user.walletAddress,
|
||||
balance,
|
||||
totalEarned: user.totalEarned,
|
||||
totalSpent: user.totalSpent,
|
||||
explorerUrl: `https://explorer.solana.com/address/${user.walletAddress}?cluster=devnet`
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Create new wallet
|
||||
router.post('/create',
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id }
|
||||
});
|
||||
|
||||
if (user?.walletAddress) {
|
||||
return res.status(400).json({ error: 'Wallet already exists' });
|
||||
}
|
||||
|
||||
// Create new Solana wallet
|
||||
const wallet = createWallet();
|
||||
const encryptedKey = encrypt(wallet.secretKey);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.id },
|
||||
data: {
|
||||
walletAddress: wallet.publicKey,
|
||||
encryptedPrivateKey: encryptedKey
|
||||
}
|
||||
});
|
||||
|
||||
// Request airdrop for testing
|
||||
await requestAirdrop(wallet.publicKey);
|
||||
|
||||
res.json({
|
||||
address: wallet.publicKey,
|
||||
message: 'Wallet created successfully. Funded with 2 SOL for transaction fees.',
|
||||
warning: 'Please backup your recovery phrase if shown. This is the only time it will be displayed.'
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Connect existing wallet
|
||||
router.post('/connect',
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const { address } = req.body;
|
||||
|
||||
if (!address) {
|
||||
return res.status(400).json({ error: 'Wallet address required' });
|
||||
}
|
||||
|
||||
// Check if address is already connected to another user
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: {
|
||||
walletAddress: address,
|
||||
NOT: { id: req.user!.id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ error: 'Wallet already connected to another account' });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.id },
|
||||
data: { walletAddress: address }
|
||||
});
|
||||
|
||||
res.json({
|
||||
address,
|
||||
message: 'Wallet connected successfully'
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Get recovery phrase (only shown once at creation)
|
||||
router.post('/backup',
|
||||
authenticate,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.id }
|
||||
});
|
||||
|
||||
if (!user?.encryptedPrivateKey) {
|
||||
return res.status(400).json({ error: 'No wallet found' });
|
||||
}
|
||||
|
||||
// Decrypt and return private key for backup
|
||||
const privateKey = decrypt(user.encryptedPrivateKey);
|
||||
|
||||
res.json({
|
||||
privateKey,
|
||||
warning: 'Store this securely. Never share it with anyone.'
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Admin: Get user's wallet
|
||||
router.get('/admin/:userId',
|
||||
authenticate,
|
||||
requireAdmin,
|
||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.params.userId },
|
||||
select: {
|
||||
id: true,
|
||||
plexUsername: true,
|
||||
walletAddress: true,
|
||||
totalEarned: true,
|
||||
totalSpent: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
let balance = 0;
|
||||
if (user.walletAddress) {
|
||||
balance = await getTokenBalance(user.walletAddress);
|
||||
}
|
||||
|
||||
res.json({
|
||||
...user,
|
||||
balance
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
export { router as walletRouter };
|
||||
Reference in New Issue
Block a user