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
+27
View File
@@ -0,0 +1,27 @@
[features]
seeds = false
skip-lint = false
[programs.localnet]
coop_credits = "CoopCreDits1111111111111111111111111111111"
[programs.devnet]
coop_credits = "CoopCreDits1111111111111111111111111111111"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
[test]
startup_wait = 10000
[test.validator]
bind_address = "0.0.0.0"
url = "https://api.devnet.solana.com"
ledger = ".anchor/test-ledger"
+3596
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
[workspace]
members = [
"programs/*"
]
resolver = "2"
[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1
[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1
+28
View File
@@ -0,0 +1,28 @@
{
"name": "coop-credits-anchor",
"version": "1.0.0",
"description": "CoopCredits SPL Token Program",
"scripts": {
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
"build": "anchor build",
"deploy": "anchor deploy",
"test": "anchor test",
"idl:export": "anchor idl init --filepath target/idl/coop_credits.json CoopCreDits1111111111111111111111111111111"
},
"dependencies": {
"@coral-xyz/anchor": "^0.29.0",
"@solana/spl-token": "^0.3.9",
"@solana/web3.js": "^1.87.6"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"chai": "^4.3.4",
"mocha": "^9.0.3",
"prettier": "^2.6.2",
"ts-mocha": "^10.0.0",
"typescript": "^4.3.5"
}
}
@@ -0,0 +1,21 @@
[package]
name = "coop-credits"
version = "1.0.0"
description = "CoopCredits SPL Token Program"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "coop_credits"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
anchor-lang = { version = "0.29.0", features = ["init-if-needed"] }
anchor-spl = { version = "0.29.0", features = ["metadata"] }
solana-program = "=1.16.20"
@@ -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,
}
+95
View File
@@ -0,0 +1,95 @@
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { CoopCredits } from "../target/types/coop_credits";
import { expect } from "chai";
describe("coop-credits", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.CoopCredits as Program<CoopCredits>;
const provider = anchor.getProvider();
let globalStatePDA: anchor.web3.PublicKey;
let mintPDA: anchor.web3.PublicKey;
let mintAuthorityPDA: anchor.web3.PublicKey;
before(async () => {
// Find PDAs
[globalStatePDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("global_state")],
program.programId,
);
[mintAuthorityPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("mint_authority")],
program.programId,
);
});
it("Initializes the program", async () => {
const tx = await program.methods
.initialize()
.accounts({
globalState: globalStatePDA,
authority: provider.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
})
.rpc();
console.log("Initialize transaction signature", tx);
// Verify global state
const globalState = await program.account.globalState.fetch(globalStatePDA);
expect(globalState.authority.toString()).to.equal(
provider.publicKey.toString(),
);
expect(globalState.paused).to.be.false;
expect(globalState.creditsPerMinute).to.equal(10);
});
it("Updates minting rate", async () => {
const newRate = 20;
const tx = await program.methods
.updateMintingRate(newRate)
.accounts({
globalState: globalStatePDA,
authority: provider.publicKey,
})
.rpc();
console.log("Update rate transaction signature", tx);
const globalState = await program.account.globalState.fetch(globalStatePDA);
expect(globalState.creditsPerMinute).to.equal(newRate);
});
it("Pauses and resumes minting", async () => {
// Pause
let tx = await program.methods
.pauseMinting()
.accounts({
globalState: globalStatePDA,
authority: provider.publicKey,
})
.rpc();
let globalState = await program.account.globalState.fetch(globalStatePDA);
expect(globalState.paused).to.be.true;
// Resume
tx = await program.methods
.resumeMinting()
.accounts({
globalState: globalStatePDA,
authority: provider.publicKey,
})
.rpc();
globalState = await program.account.globalState.fetch(globalStatePDA);
expect(globalState.paused).to.be.false;
});
});