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:
2026-04-14 11:09:50 -04:00
parent f106328f6a
commit e8d9b1fd42
69 changed files with 11382 additions and 0 deletions
@@ -0,0 +1,251 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Burn, Mint, MintTo, Token, TokenAccount};
declare_id!("CoopCredits111111111111111111111111111111111");
pub const DECIMALS: u8 = 6;
#[program]
pub mod coop_credits {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let state = &mut ctx.accounts.global_state;
state.authority = ctx.accounts.authority.key();
state.mint = ctx.accounts.mint.key();
state.total_minted = 0;
state.total_burned = 0;
state.paused = false;
state.rate = 10;
state.bump = ctx.bumps.global_state;
Ok(())
}
pub fn mint_to_user(
ctx: Context<MintToUser>,
amount: u64,
session_id: String,
title: String,
minutes: u32,
) -> Result<()> {
require!(!ctx.accounts.state.paused, ErrorCode::Paused);
require!(
ctx.accounts.authority.key() == ctx.accounts.state.authority,
ErrorCode::Unauthorized
);
let seeds = &[b"mint_auth", &[ctx.bumps.mint_auth]];
let signer = &[&seeds[..]];
let cpi = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.user_token.to_account_info(),
authority: ctx.accounts.mint_auth.to_account_info(),
};
token::mint_to(
CpiContext::new_with_signer(ctx.accounts.token_prog.to_account_info(), cpi, signer),
amount,
)?;
let state = &mut ctx.accounts.state;
state.total_minted = state.total_minted.saturating_add(amount);
let tx = &mut ctx.accounts.tx_record;
tx.user = ctx.accounts.user_token.owner;
tx.tx_type = TxType::Earn;
tx.amount = amount;
tx.timestamp = Clock::get()?.unix_timestamp;
tx.session_id = session_id;
tx.title = title;
tx.minutes = minutes;
msg!("Minted {}", amount);
Ok(())
}
pub fn burn_from_user(
ctx: Context<BurnFromUser>,
amount: u64,
request_id: String,
title: String,
) -> Result<()> {
require!(!ctx.accounts.state.paused, ErrorCode::Paused);
let cpi = Burn {
mint: ctx.accounts.mint.to_account_info(),
from: ctx.accounts.user_token.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
};
token::burn(
CpiContext::new(ctx.accounts.token_prog.to_account_info(), cpi),
amount,
)?;
let state = &mut ctx.accounts.state;
state.total_burned = state.total_burned.saturating_add(amount);
let tx = &mut ctx.accounts.tx_record;
tx.user = ctx.accounts.user.key();
tx.tx_type = TxType::Spend;
tx.amount = amount;
tx.timestamp = Clock::get()?.unix_timestamp;
tx.session_id = request_id;
tx.title = title;
tx.minutes = 0;
msg!("Burned {}", amount);
Ok(())
}
pub fn set_rate(ctx: Context<Admin>, new_rate: u32) -> Result<()> {
require!(
ctx.accounts.authority.key() == ctx.accounts.state.authority,
ErrorCode::Unauthorized
);
ctx.accounts.state.rate = new_rate;
Ok(())
}
pub fn pause(ctx: Context<Admin>) -> Result<()> {
require!(
ctx.accounts.authority.key() == ctx.accounts.state.authority,
ErrorCode::Unauthorized
);
ctx.accounts.state.paused = true;
Ok(())
}
pub fn resume(ctx: Context<Admin>) -> Result<()> {
require!(
ctx.accounts.authority.key() == ctx.accounts.state.authority,
ErrorCode::Unauthorized
);
ctx.accounts.state.paused = false;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = authority, space = 8 + State::SIZE, seeds = [b"state"], bump)]
pub global_state: Account<'info, State>,
#[account(init, payer = authority, mint::decimals = DECIMALS, mint::authority = mint_auth)]
pub mint: Account<'info, Mint>,
/// CHECK: PDA
#[account(seeds = [b"mint_auth"], bump)]
pub mint_auth: UncheckedAccount<'info>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
pub token_prog: Program<'info, Token>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
#[instruction(amount: u64, session_id: String, title: String, minutes: u32)]
pub struct MintToUser<'info> {
#[account(mut, seeds = [b"state"], bump = state.bump)]
pub state: Account<'info, State>,
#[account(mut)]
pub mint: Account<'info, Mint>,
/// CHECK: PDA
#[account(seeds = [b"mint_auth"], bump)]
pub mint_auth: UncheckedAccount<'info>,
#[account(mut)]
pub user_token: Account<'info, TokenAccount>,
#[account(init, payer = authority, space = 8 + TxRecord::SIZE, seeds = [b"tx", user_token.owner.as_ref(), &Clock::get()?.unix_timestamp.to_le_bytes()], bump)]
pub tx_record: Account<'info, TxRecord>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
pub token_prog: Program<'info, Token>,
}
#[derive(Accounts)]
#[instruction(amount: u64, request_id: String, title: String)]
pub struct BurnFromUser<'info> {
#[account(mut, seeds = [b"state"], bump = state.bump)]
pub state: Account<'info, State>,
#[account(mut)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub user_token: Account<'info, TokenAccount>,
#[account(init, payer = authority, space = 8 + TxRecord::SIZE, seeds = [b"tx", user.key().as_ref(), &Clock::get()?.unix_timestamp.to_le_bytes()], bump)]
pub tx_record: Account<'info, TxRecord>,
#[account(mut)]
pub authority: Signer<'info>,
/// CHECK: User signer
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
pub token_prog: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct Admin<'info> {
#[account(mut, seeds = [b"state"], bump = state.bump)]
pub state: Account<'info, State>,
pub authority: Signer<'info>,
}
#[account]
pub struct State {
pub authority: Pubkey,
pub mint: Pubkey,
pub total_minted: u64,
pub total_burned: u64,
pub paused: bool,
pub rate: u32,
pub bump: u8,
}
impl State {
pub const SIZE: usize = 32 + 32 + 8 + 8 + 1 + 4 + 1;
}
#[account]
pub struct TxRecord {
pub user: Pubkey,
pub tx_type: TxType,
pub amount: u64,
pub timestamp: i64,
pub session_id: String,
pub title: String,
pub minutes: u32,
}
impl TxRecord {
pub const SIZE: usize = 32 + 1 + 8 + 8 + 68 + 132 + 4;
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq)]
pub enum TxType {
Earn,
Spend,
}
#[error_code]
pub enum ErrorCode {
#[msg("Paused")]
Paused,
#[msg("Unauthorized")]
Unauthorized,
}