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,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,
|
||||
}
|
||||
Reference in New Issue
Block a user