Skip to content

Conversation

Absterrg0
Copy link

🔄 Octasol Platform Enhancement: Complete Escrow System & Admin Management

📋 Overview

This PR implements a comprehensive enhancement to the Octasol bounty platform, introducing a complete Solana-based escrow payment system with USDC integration, GitHub bot automation, dynamic admin management, and robust error handling flows. These changes transform Octasol from a basic bounty platform into a production-ready, trustless payment system.

🤖 GitHub Bot Integration

Webhook System Implementation

Core Webhook Handler (/api/webhook/github/route.ts)

// Comprehensive webhook processing with signature verification
export async function POST(req: NextRequest) {
  // 1. Verify GitHub webhook signature for security
  // 2. Parse and validate webhook payload
  // 3. Route events to appropriate handlers
  // 4. Log all events for monitoring
}

Event Handlers

Installation Events:

async function handleInstallationEvent(event: GitHubWebhookEvent) {
  switch (action) {
    case 'created': // App installed
    case 'deleted': // App uninstalled
    case 'suspend': // App suspended
    case 'unsuspend': // App unsuspended
  }
}

Pull Request Events:

async function handlePullRequestEvent(event: GitHubWebhookEvent) {
  switch(action) {
    case "opened": // Check for linked issues and create submissions
    case "closed": // Handle merge/close scenarios
  }
}

Issue Events:

async function handleIssueEvent(event: GitHubWebhookEvent) {
  if (action === 'closed') {
    await handleIssueClosed(repository.full_name, issue.number, installation.id);
  }
}

Automated Workflows

1. PR Submission Detection

export async function checkPRforLinkedIssue(body: string, repoName: string, installationId: number, pullRequestNumber: number, githubId: number) {
  // 1. Extract issue number from PR body/title
  // 2. Validate wallet address (PR description or user profile)
  // 3. Create submission record
  // 4. Post automated GitHub comments
}

2. Payment Release Automation

export async function releasePayment(repoName: string, prNumber: number, installationId: number) {
  // 1. Verify winner submission exists
  // 2. Execute Solana smart contract transaction
  // 3. Update database status
  // 4. Post confirmation comment
}

💰 Solana Escrow Payment System with USDC

Smart Contract Integration

Program Details

  • Program Address: GsYHXAJGQ25hA8MLeVXMuXkUPdiQ76k3QQBuYtmVFShp (On devnet)
  • Token: USDC (Configurable via NEXT_PUBLIC_USDC_MINT_ADDRESS)
  • Network: Solana Devnet/Mainnet (Configurable)

Key Smart Contract Instructions

// 1. Initialize Bounty (Create Escrow)
await program.methods
  .initializeBounty(bountyIdBN, amountBN)
  .accounts({
    maintainer: wallet.publicKey,
    bounty: bountyAccountKp.publicKey,
    maintainerTokenAccount: maintainerTokenAccount,
    escrowAuthority: escrowAuthorityPda,
    escrowTokenAccount: escrowTokenAccount,
    config: configPda,
    mint: USDCMint,
    // ... additional accounts
  })
  .signers([bountyAccountKp])
  .rpc();

// 2. Assign Contributor
await program.methods
  .assignContributor()
  .accounts({
    maintainer: wallet.publicKey,
    bounty: bountyKeypair.publicKey,
    contributor: new PublicKey(submission.walletAddress),
    systemProgram: SystemProgram.programId,
  })
  .rpc();

// 3. Complete Bounty (Release Payment)
await program.methods
  .completeBounty(bountyIdBN)
  .accounts({
    bounty: bountyAccountKp.publicKey,
    escrowAuthority: escrowAuthorityPda,
    maintainer: serverWallet.publicKey,
    contributor: winnerAccount,
    contributorTokenAccount: contributorTokenAccount,
    escrowTokenAccount: escrowTokenAccount,
    // ... additional accounts
  })
  .rpc();

Escrow Workflow Implementation

1. Bounty Creation & Escrow Setup

// BountyDialog.tsx - Complete escrow setup flow
const onSubmit = async (data: BountyFormData) => {
  // 1. Create bounty record in database
  const { response } = await POST("/create-bounty", payload);
  
  // 2. Setup Solana transaction
  const bountyAccountKp = generateBountyKeypair(bountyId!);
  const [escrowAuthorityPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("escrow_auth"), bountyAccountKp.publicKey.toBuffer()],
    program.programId
  );
  
  // 3. Execute blockchain transaction
  const blockchainTxSignature = await program.methods
    .initializeBounty(bountyIdBN, amountBN)
    .accounts({ /* ... */ })
    .rpc();
    
  // 4. Update database with transaction details
  await updateEscrowedBounty(bountyId, {
    status: 2,
    escrowPda: escrowAuthorityPda.toString(),
    transactionHash: blockchainTxSignature
  });
};

2. Contributor Assignment

// EscrowDialog.tsx - Assign winner to bounty
const handleCreateEscrow = async (submission: Submission) => {
  // 1. Validate wallet connection and submission
  // 2. Execute smart contract assignment
  const txHash = await program.methods
    .assignContributor()
    .accounts({ /* ... */ })
    .rpc();
    
  // 3. Update submission status
  await POST(`/updateSubmissionStatus`, {
    submissionId: submission.id,
    githubId: user.githubId,
  });
};

3. Payment Release

// Automatic release on PR merge
export async function releasePayment(repoName: string, prNumber: number, installationId: number) {
  // 1. Find winner submission
  const winnerSubmission = await db.submission.findFirst({
    where: { githubPRNumber: prNumber, status: 2 }
  });
  
  // 2. Execute payment transaction
  const txSignature = await program.methods
    .completeBounty(bountyIdBN)
    .accounts({ /* ... */ })
    .rpc();
    
  // 3. Update database and post confirmation
  await db.$transaction([
    db.submission.update({ where: { id: winnerSubmission.id }, data: { status: 4 } }),
    db.bounty.update({ where: { id: winnerSubmission.bountyId }, data: { status: 3 } })
  ]);
}

🔄 Advanced Flow Management & Error Handling

Early Merging Detection

Scenario Handling

// Handle different PR merge scenarios
export async function handleDifferentPRMerged(repoName: string, prNumber: number, installationId: number, issueNumber: number) {
  const bounty = await db.bounty.findFirst({
    where: { repoName: repoName, issueNumber: issueNumber },
    include: { submissions: { where: { status: 2 } } }
  });

  if (bounty.submissions.length > 0) {
    // Conflict with existing winner - post conflict message
    commentBody = `## ⚠️ Bounty Conflict Detected
    
A different pull request has been merged while there was an active bounty with an assigned contributor.
The bounty has been moved to "conflicted" state for admin review.`;
  } else {
    // Conflict with unassigned bounty
    commentBody = `## ⚠️ Bounty Conflict Detected
    
A pull request has been merged while there was an active bounty that had not been assigned yet.
The bounty has been moved to "conflicted" state for admin review.`;
  }
  
  // Move bounty to conflicted state (status 8)
  await db.bounty.update({ where: { id: bounty.id }, data: { status: 8 } });
}

No Contributor Assigned Protection

Validation Checks

// Check for winner submission before payment
if (!winnerSubmission) {
  const commentBody = `## ⚠️ Contributor Not Assigned

You merged this bounty without assigning a contributor.
To ensure the bounty is paid out correctly, you must first assign a contributor via Octasol before merging the pull request.`;

  await axios.post(`https://api.github.com/repos/${repoName}/issues/${prNumber}/comments`, {
    body: commentBody
  }, { headers: { Authorization: `token ${accessToken}` } });

  return { success: false, error: 'No winner submission found' };
}

Issue Closure Handling

Automatic Conflict Detection

export async function handleIssueClosed(repoName: string, issueNumber: number, installationId: number) {
  const bounty = await db.bounty.findFirst({
    where: { repoName: repoName, issueNumber: issueNumber }
  });

  if (bounty) {
    // Check if issue was closed by winner PR
    const issueResponse = await axios.get(`https://api.github.com/repos/${repoName}/issues/${issueNumber}`);
    const issue = issueResponse.data;
    
    if (issue.pull_request && issue.pull_request.merged_at) {
      const mergedPRNumber = issue.pull_request.number;
      if (mergedPRNumber === winnerSubmission.githubPRNumber) {
        // Winner PR merged - don't move to conflicted state
        return;
      }
    }
    
    // Move to conflicted state for admin review
    await db.bounty.update({ where: { id: bounty.id }, data: { status: 8 } });
  }
}

👥 Dynamic Admin Management System

Database-Driven Admin Validation

Admin Table Implementation

// lib/constants.ts - Replace hardcoded admin checks
export async function isAdmin(githubLogin: string): Promise<boolean> {
  if (!githubLogin) return false;
  
  try {
    const admin = await db.admin.findFirst({
      where: { githubName: githubLogin.toLowerCase() }
    });
    return !!admin;
  } catch (error) {
    console.error("Error checking admin status:", error);
    return false;
  }
}

Admin Management API

// /api/admin/populate/route.ts - Secure admin management
export async function POST(req: NextRequest) {
  // 1. Verify admin token for security
  const authToken = req.headers.get("x-admin-token");
  const expectedToken = process.env.ADMIN_TOKEN;
  
  if (authToken !== expectedToken) {
    await logToDiscord(`🚨 Unauthorized Admin Population Attempt`, "WARN");
    return NextResponse.json({ error: "Invalid admin token" }, { status: 403 });
  }
  
  // 2. Clear existing admins and populate with new ones
  const { githubUsernames } = await req.json();
  const result = await db.$transaction(async (tx) => {
    await tx.admin.deleteMany({}); // Clear existing
    return await Promise.all(
      githubUsernames.map(username => 
        tx.admin.create({ data: { githubName: username.trim().toLowerCase() } })
      )
    );
  });
  
  // 3. Log successful operation
  await logToDiscord(`✅ Admin Table Populated\nAdmins: ${githubUsernames.join(', ')}`, "INFO");
}

Admin Dashboard Integration

// Admin bounties page with dynamic admin checks
export default function AdminBountiesPage() {
  useEffect(() => {
    const checkAdminStatus = async () => {
      if (user?.login) {
        const adminStatus = await isAdmin(user.login);
        if (!adminStatus) {
          router.back(); // Redirect non-admins
        }
      }
    };
    checkAdminStatus();
  }, [user, router]);
}

🔧 Environment Variable Flexibility

Comprehensive Configuration System

Required Environment Variables

See all required environment variables in the .env.example file.
Make sure to copy this file to .env.local and fill in the necessary values before running the application.

Configuration Validation

// Environment variable validation throughout the codebase
const SERVER_WALLET_SECRET_KEY = process.env.ADMIN_PRIVATE_KEY;
if (!SERVER_WALLET_SECRET_KEY) {
  throw new Error('ADMIN_PRIVATE_KEY environment variable is not set.');
}

const USDCMintAddress = process.env.NEXT_PUBLIC_USDC_MINT_ADDRESS || "";
if (!USDCMintAddress) {
  throw new Error('USDC mint address not configured.');
}

🔄 Migration Guide

Database Migration

# Apply all migrations from wallet_address onwards 
npx prisma migrate deploy

npx prisma generate --no-engine

Environment Setup

# Copy and configure environment variables
cp .env.example .env.local

# Populate admin table (secure endpoint)
curl -X POST /api/admin/populate \
  -H "x-admin-token: your_admin_token" \
  -d '{"githubUsernames": ["admin1", "admin2"]}'

Smart Contract Deployment

# Deploy to Solana (if not already deployed)
anchor build
anchor deploy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant