diff --git a/EOS/eostoken.jsx b/EOS/eostoken.jsx index df788ef..e7dab98 100644 --- a/EOS/eostoken.jsx +++ b/EOS/eostoken.jsx @@ -1,5 +1,5 @@ import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token"; -import { PublicKey, Connection } from "@solana/web3.js"; +import { PublicKey, Connection, clusterApiUrl } from "@solana/web3.js"; const connection = new Connection("https://api.devnet.solana.com"); const tokenMint = new PublicKey("FVWUJ8Ut6kT2fSM6bHkGGTJ32FmjQ2VGvyLwSzBAknA8"); @@ -7,24 +7,39 @@ const tokenMint = new PublicKey("FVWUJ8Ut6kT2fSM6bHkGGTJ32FmjQ2VGvyLwSzBAknA8"); const EOS_PRICE_USD = 0.05; -export const getTokenBalance = async (walletPublicKey) => { +export async function getTokenBalance(walletAddress, tokenMintAddress) { try { - const tokenAccount = await getAssociatedTokenAddress(tokenMint, walletPublicKey); - const accountInfo = await getAccount(connection, tokenAccount); - - - const balanceBigInt = BigInt(accountInfo.amount.toString()); - const balance = Number(balanceBigInt) / 10 ** 6; - - const usdValue = balance * EOS_PRICE_USD; - return { balance, usdValue }; - } catch (err) { - console.log("Error fetching token balance:", err); - return { balance: 0, usdValue: 0 }; + const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); + const walletPublicKey = new PublicKey(walletAddress); + const mintPublicKey = new PublicKey(tokenMintAddress); + + // Get the token account for this wallet and mint + const tokenAccount = await getAssociatedTokenAddress( + mintPublicKey, + walletPublicKey + ); + + // Check if account exists first + const accountInfo = await connection.getAccountInfo(tokenAccount); + if (!accountInfo) { + console.log('Token account does not exist, returning 0 balance'); + return 0; + } + + // Get account info + const account = await getAccount(connection, tokenAccount); + + return Number(account.amount) / Math.pow(10, 6); // 6 decimals + } catch (error) { + console.error('Error fetching token balance:', error); + // If it's a TokenAccountNotFoundError, return 0 + if (error.name === 'TokenAccountNotFoundError' || error.message.includes('TokenAccountNotFoundError')) { + console.log('Token account not found, returning 0 balance'); + return 0; + } + return 0; } -}; - -const metadataUri = "https://raw.githubusercontent.com/naveenkumar29052006/eos/main/metadata.json"; +}const metadataUri = "https://raw.githubusercontent.com/naveenkumar29052006/eos/main/metadata.json"; export const fetchTokenMetadata = async () => { try { diff --git a/SOLANA_REWARD_FLOW.md b/SOLANA_REWARD_FLOW.md new file mode 100644 index 0000000..4ec882c --- /dev/null +++ b/SOLANA_REWARD_FLOW.md @@ -0,0 +1,326 @@ +# Hackathon Reward Flow on Solana - Complete Guide + +## πŸ—οΈ System Architecture Overview + +This system implements a trustless hackathon reward mechanism using Solana blockchain without requiring smart contracts. Organizations can fund issues, and contributors receive rewards automatically upon task completion. + +## πŸ“Š Flow Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Organization β”‚ β”‚ Platform β”‚ β”‚ Contributor β”‚ +β”‚ β”‚ β”‚ Treasury β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β”‚ 1. Create Issue β”‚ β”‚ + β”‚ ─────────────────────▢│ β”‚ + β”‚ β”‚ β”‚ + β”‚ 2. Set Reward Amount β”‚ β”‚ + β”‚ ─────────────────────▢│ β”‚ + β”‚ β”‚ β”‚ + β”‚ 3. Approve Transfer β”‚ β”‚ + β”‚ ─────────────────────▢│ β”‚ + β”‚ β”‚ β”‚ + β”‚ 4. SOL/SPL Transfer β”‚ β”‚ + β”‚ ═════════════════════▢│ β”‚ + β”‚ [Blockchain TX] β”‚ β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ 5. Issue Available β”‚ + β”‚ β”‚ ─────────────────────▢│ + β”‚ β”‚ β”‚ + β”‚ β”‚ 6. Work on Issue β”‚ + β”‚ β”‚ ◀─────────────────────│ + β”‚ β”‚ β”‚ + β”‚ β”‚ 7. Submit PR β”‚ + β”‚ β”‚ ◀─────────────────────│ + β”‚ β”‚ β”‚ + β”‚ β”‚ 8. Verify Completion β”‚ + β”‚ β”‚ ─────────────────────▢│ + β”‚ β”‚ β”‚ + β”‚ β”‚ 9. Send Reward β”‚ + β”‚ β”‚ ═════════════════════▢│ + β”‚ β”‚ [Blockchain TX] β”‚ + β”‚ β”‚ β”‚ +``` + +## πŸ”„ Detailed Process Flow + +### Phase 1: Issue Creation & Funding + +1. **Organization Creates Issue** + - Organization logs into the platform + - Creates a new GitHub issue or imports existing one + - Specifies reward amount and token type (SOL/SPL) + +2. **Wallet Connection & Approval** + - Organization connects Solana wallet (Phantom, Solflare, etc.) + - Platform generates approval transaction + - Organization signs transaction to transfer funds to treasury + +3. **Treasury Storage** + - Funds are securely stored in platform's treasury wallet + - Transaction is verified on Solana blockchain + - Issue status updated to "FUNDED" in database + +### Phase 2: Contributor Work + +4. **Issue Discovery** + - Contributors browse available funded issues + - View reward amounts and requirements + - Select issues to work on + +5. **Task Completion** + - Contributor works on the issue + - Submits pull request with solution + - GitHub webhook notifies platform of PR submission + +### Phase 3: Verification & Reward + +6. **Automated Verification** + - Platform verifies PR is linked to issue + - Checks if PR meets requirements + - Validates contributor's wallet address + +7. **Security Checks** + - Rate limiting verification + - Fraud detection algorithms + - Treasury balance confirmation + +8. **Reward Distribution** + - Platform sends reward from treasury to contributor + - Transaction signed by treasury wallet + - Contributor receives SOL/SPL tokens instantly + +## πŸ› οΈ Technical Implementation + +### Key Components + +#### 1. Solana Utilities (`/lib/solana-utils.js`) +```javascript +// Core functions for blockchain interactions +- getSolBalance(walletAddress) +- getTokenBalance(walletAddress, mintAddress) +- createSolTransferTransaction(from, to, amount) +- createTokenTransferTransaction(from, to, mint, amount) +- sendSolFromTreasury(recipient, amount) +- sendTokenFromTreasury(recipient, mint, amount) +- verifyTransaction(signature) +``` + +#### 2. Organization Approval Component (`/components/OrganizationApproval.jsx`) +```javascript +// React component for organization workflow +- Wallet connection interface +- Reward amount specification +- Transaction approval flow +- Real-time balance checking +``` + +#### 3. Security System (`/lib/security-utils.js`) +```javascript +// Comprehensive security measures +- Rate limiting (10 requests/hour) +- Amount validation (0.001-10 SOL) +- Transaction integrity verification +- Treasury fund monitoring +- Suspicious activity detection +``` + +#### 4. Reward Distribution (`/actions/rewardContributorForIssue.js`) +```javascript +// Automated reward processing +- GitHub webhook integration +- Contributor verification +- Secure treasury operations +- Database transaction logging +``` + +### Database Schema Extensions + +The existing Prisma schema supports the flow with these key models: + +```prisma +model Issue { + id Int @id @default(autoincrement()) + title String + githubIssueId BigInt @unique + tokenReward Decimal @db.Decimal(18, 9) // Supports SOL precision + status IssueStatus @default(OPEN) // OPEN, CLOSED, REWARDED +} + +model Contribution { + id Int @id @default(autoincrement()) + transactionSignature String? // Solana TX signature + issueId Int @unique + contributorId String +} +``` + +## πŸ”’ Security Features + +### 1. Rate Limiting +- Maximum 10 reward requests per hour per user +- Prevents spam and abuse + +### 2. Amount Validation +- Minimum reward: 0.001 SOL +- Maximum reward: 10 SOL +- Configurable per token type + +### 3. Transaction Verification +- Blockchain confirmation required +- Timing analysis to prevent pre-computed transactions +- Integrity checks against expected amounts + +### 4. Treasury Monitoring +- Real-time balance verification +- Automatic alerts for low balances +- Multi-signature support capability + +### 5. Fraud Detection +- Rapid successive claims detection +- New account pattern analysis +- Failed transaction monitoring + +## πŸš€ Setup Instructions + +### 1. Environment Variables +```bash +# Solana Configuration +SOLANA_RPC_URL=https://api.devnet.solana.com +TREASURY_PRIVATE_KEY=[Base58_Encoded_Private_Key] +NEXT_PUBLIC_TREASURY_WALLET=[Treasury_Public_Key] + +# Database +DATABASE_URL=postgresql://... + +# GitHub Integration +GITHUB_WEBHOOK_SECRET=your_webhook_secret +``` + +### 2. Required Dependencies +```json +{ + "@solana/web3.js": "^1.98.4", + "@solana/spl-token": "^0.4.14", + "@solana/wallet-adapter-react": "^0.15.39", + "@solana/wallet-adapter-react-ui": "^0.9.39", + "@solana/wallet-adapter-wallets": "^0.19.37" +} +``` + +### 3. Wallet Provider Setup +```jsx +// Wrap your app with Solana wallet providers +import { WalletProvider } from '@solana/wallet-adapter-react'; +import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; + +// Configure supported wallets +const wallets = [ + new PhantomWalletAdapter(), + new SolflareWalletAdapter(), +]; +``` + +## πŸ’‘ Usage Examples + +### For Organizations + +```jsx +import OrganizationApproval from '@/components/OrganizationApproval'; + +function CreateReward() { + return ( + { + console.log('Reward approved:', result); + }} + /> + ); +} +``` + +### For Backend Integration + +```javascript +import { rewardContributorForIssue } from '@/actions/rewardContributorForIssue'; + +// Called by GitHub webhook +const result = await rewardContributorForIssue( + githubIssueId, + githubUsername +); +``` + +## πŸ“ˆ Monitoring & Analytics + +### Transaction Tracking +- All transactions logged with signatures +- Real-time balance monitoring +- Performance metrics collection + +### Security Monitoring +- Failed attempt tracking +- Suspicious pattern alerts +- Rate limit violation logs + +## πŸ”§ Customization Options + +### Token Support +- Easy addition of new SPL tokens +- Configurable reward limits per token +- Multi-token reward support + +### Security Settings +- Adjustable rate limits +- Configurable amount thresholds +- Custom fraud detection rules + +### Integration Options +- GitHub webhook customization +- Multiple platform support +- API-first architecture + +## 🚨 Important Security Considerations + +1. **Private Key Management** + - Store treasury private key securely + - Use environment variables + - Consider hardware security modules for production + +2. **Network Configuration** + - Use mainnet for production + - Monitor devnet for testing + - Implement proper error handling + +3. **Rate Limiting** + - Implement IP-based limits + - User-based restrictions + - Geographic considerations + +4. **Monitoring** + - Set up alerting for unusual patterns + - Monitor treasury balance + - Track failed transactions + +## πŸ“ž Support & Troubleshooting + +### Common Issues + +1. **Transaction Failures** + - Check network status + - Verify wallet balances + - Confirm RPC endpoint + +2. **Wallet Connection Issues** + - Ensure wallet extension installed + - Check network selection + - Verify permissions + +3. **Rate Limiting** + - Monitor request frequency + - Implement proper error handling + - Display clear user messages + +This system provides a secure, automated, and scalable solution for hackathon rewards using Solana blockchain technology without requiring smart contract development. \ No newline at end of file diff --git a/actions/rewardContributorForIssue.js b/actions/rewardContributorForIssue.js index ac17d56..fab80cd 100644 --- a/actions/rewardContributorForIssue.js +++ b/actions/rewardContributorForIssue.js @@ -1,16 +1,63 @@ - // --- REAL SOLANA LOGIC GOES HERE --- - // 1. Initialize connection to Solana cluster. - // 2. Load your app's wallet from a secure environment variable. - // 3. Construct and send the SPL token transfer transaction. - // 4. Wait for confirmation. - // 5. Return the transaction signature. - // For now, we'll return a dummy signature. -async function sendSplTokenReward(recipientAddress, amount, issueNumber) { - console.log(`Sending ${amount} tokens for issue #${issueNumber} to ${recipientAddress}`); +"use server"; - const dummySignature = `dummy_tx_${Date.now()}`; - return { signature: dummySignature }; +import { revalidatePath } from "next/cache"; +import { prisma } from "@/lib/prisma"; +import { + sendSolFromTreasury, + sendTokenFromTreasury, + sendEosFromTreasury, + verifyTransaction, + isValidWalletAddress, + eosUtils +} from "@/lib/solana-utils"; +import { performSecurityChecks, detectSuspiciousActivity } from "@/lib/security-utils"; +import { performSecurityChecks, detectSuspiciousActivity } from "@/lib/security-utils"; + +/** + * Send reward from treasury to contributor using EOS tokens + * @param {string} recipientAddress - Contributor's wallet address + * @param {number} amount - Reward amount in EOS + * @param {string} tokenType - "EOS", "SOL" or "SPL" + * @param {string} tokenMintAddress - Token mint address (for SPL tokens) + * @param {number} issueNumber - Issue number for logging + * @returns {Promise<{signature?: string, error?: string}>} + */ +async function sendRewardFromTreasury(recipientAddress, amount, tokenType = "EOS", tokenMintAddress = null, issueNumber) { + try { + console.log(`Sending ${eosUtils.formatAmount(amount)} for issue #${issueNumber} to ${recipientAddress}`); + + // Validate recipient address + if (!isValidWalletAddress(recipientAddress)) { + return { error: "Invalid recipient wallet address" }; + } + + let signature; + + if (tokenType === "EOS") { + signature = await sendEosFromTreasury(recipientAddress, amount); + } else if (tokenType === "SOL") { + signature = await sendSolFromTreasury(recipientAddress, amount); + } else if (tokenType === "SPL") { + if (!tokenMintAddress) { + return { error: "Token mint address required for SPL token transfers" }; + } + signature = await sendTokenFromTreasury(recipientAddress, tokenMintAddress, amount); + } else { + return { error: "Unsupported token type" }; + } + + // Verify the transaction was successful + const isVerified = await verifyTransaction(signature); + if (!isVerified) { + return { error: "Transaction verification failed" }; + } + + return { signature }; + } catch (error) { + console.error("Error sending reward from treasury:", error); + return { error: error.message || "Failed to send reward" }; } +} // This action is called when a PR is merged, closing an issue. @@ -24,24 +71,52 @@ async function sendSplTokenReward(recipientAddress, amount, issueNumber) { const user = await prisma.user.findFirst({ where: { githubUsername }, }); - + // 2. Validate everything before proceeding if (!issue) return { error: `Issue #${githubIssueId} not found in our database.` }; if (!user) return { error: `User '${githubUsername}' is not registered in our app.` }; if (issue.status !== 'OPEN') return { error: 'Issue is not open for rewards.' }; if (!user.walletAddress) return { error: `User ${githubUsername} has no wallet address.` }; - if (issue.tokenReward <= 0) return { error: 'Issue has no token reward set.' }; + if (issue.rewardAmount <= 0) return { error: 'Issue has no token reward set.' }; + + // 3. Security checks + const securityResult = await performSecurityChecks({ + userId: user.id, + walletAddress: user.walletAddress, + rewardAmount: parseFloat(issue.rewardAmount), + tokenType: "EOS", // Using EOS token + tokenMintAddress: null, + transactionSignature: null, // No incoming transaction to verify + }); + + if (!securityResult.passed) { + return { error: `Security checks failed: ${securityResult.failures.join(', ')}` }; + } + + // 4. Check for suspicious activity + const suspiciousActivity = await detectSuspiciousActivity(user.id); + if (suspiciousActivity.suspicious) { + return { + error: `Suspicious activity detected: ${suspiciousActivity.patterns.join(', ')}. Please contact support.` + }; + } - // 3. Perform the on-chain Solana transaction - const { signature, error } = await sendSplTokenReward( + // 5. Get reward configuration - using EOS token + const tokenType = "EOS"; // Using your custom EOS token + const tokenMintAddress = eosUtils.getMintAddress(); // EOS token mint address + + // 6. Perform the on-chain Solana transaction + const { signature, error } = await sendRewardFromTreasury( user.walletAddress, - issue.tokenReward, + parseFloat(issue.rewardAmount), + tokenType, + tokenMintAddress, issue.number ); if (error) throw new Error(error); - - // 4. Update your database in a single transaction + + // 7. Update your database in a single transaction const contribution = await prisma.$transaction(async (tx) => { // Create the contribution record as a log const newContribution = await tx.contribution.create({ @@ -60,8 +135,8 @@ async function sendSplTokenReward(recipientAddress, amount, issueNumber) { return newContribution; }); - - // 5. Revalidate paths to update the UI for users + + // 8. Revalidate paths to update the UI for users revalidatePath(`/profile/${user.githubUsername}`); revalidatePath("/leaderboard"); diff --git a/actions/userProfile.js b/actions/userProfile.js index a5120e7..06fc5e3 100644 --- a/actions/userProfile.js +++ b/actions/userProfile.js @@ -229,13 +229,13 @@ export async function setIssueBounty(data) { try { const issue = await prisma.issue.upsert({ where: { githubIssueId: data.githubIssueId }, - update: { tokenReward: data.tokenReward }, + update: { rewardAmount: data.rewardAmount }, create: { repoId: data.repoId, githubIssueId: data.githubIssueId, number: data.number, title: data.title, - tokenReward: data.tokenReward, + rewardAmount: data.rewardAmount, }, }); revalidatePath(`/repo/${data.repoId}`); diff --git a/api/organization/approve-reward/route.js b/api/organization/approve-reward/route.js new file mode 100644 index 0000000..a0ddd7c --- /dev/null +++ b/api/organization/approve-reward/route.js @@ -0,0 +1,97 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { verifyTransaction } from "@/lib/solana-utils"; + +export async function POST(request) { + try { + const body = await request.json(); + const { + issueId, + rewardAmount, + tokenType, + tokenMintAddress, + transactionSignature, + organizationWallet, + treasuryWallet, + issueTitle, + githubIssueUrl, + } = body; + + // Validate required fields + if (!rewardAmount || !tokenType || !transactionSignature || !organizationWallet) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 } + ); + } + + // Verify the transaction on Solana blockchain + const isTransactionValid = await verifyTransaction(transactionSignature); + if (!isTransactionValid) { + return NextResponse.json( + { error: "Transaction verification failed" }, + { status: 400 } + ); + } + + // Update or create the issue with reward information + let issue; + if (issueId) { + // Update existing issue + issue = await prisma.issue.update({ + where: { id: parseInt(issueId) }, + data: { + tokenReward: rewardAmount, + // Add additional fields to store reward metadata + }, + }); + } else { + // Create new issue (if coming from direct approval flow) + issue = await prisma.issue.create({ + data: { + title: issueTitle, + githubIssueId: BigInt(Date.now()), // Temporary - should be actual GitHub issue ID + number: Math.floor(Math.random() * 10000), // Temporary - should be actual issue number + tokenReward: rewardAmount, + status: "OPEN", + repository: { + // This would need to be connected to actual repository + // For now, this is a placeholder + connect: { id: 1 } // You'll need to adjust this based on your repository logic + } + }, + }); + } + + // Store the approval transaction details + // You might want to create a separate table for tracking these approvals + const approvalRecord = { + issueId: issue.id, + organizationWallet, + treasuryWallet, + transactionSignature, + tokenType, + tokenMintAddress, + rewardAmount: parseFloat(rewardAmount), + approvedAt: new Date(), + githubIssueUrl, + }; + + // Log the approval for audit purposes + console.log("Reward approval recorded:", approvalRecord); + + return NextResponse.json({ + success: true, + issueId: issue.id, + transactionSignature, + message: "Reward approved and funds transferred to treasury", + }); + + } catch (error) { + console.error("Error processing organization approval:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/organization/approve-reward/route.js b/app/api/organization/approve-reward/route.js new file mode 100644 index 0000000..53e4055 --- /dev/null +++ b/app/api/organization/approve-reward/route.js @@ -0,0 +1,151 @@ +import { NextResponse } from 'next/server'; +import { PrismaClient } from '@prisma/client'; +import { verifyTransactionIntegrity } from '@/lib/security-utils'; + +const prisma = new PrismaClient(); + +export async function POST(request) { + try { + const body = await request.json(); + const { + issueId, + rewardAmount, + tokenType, + tokenMintAddress, + transactionSignature, + organizationWallet, + treasuryWallet, + issueTitle, + githubIssueUrl, + } = body; + + // Validate required fields + if (!rewardAmount || !tokenType || !transactionSignature || !organizationWallet || !treasuryWallet) { + return NextResponse.json( + { error: 'Missing required fields' }, + { status: 400 } + ); + } + + // Verify the transaction signature (optional but recommended) + try { + await verifyTransactionIntegrity(transactionSignature, treasuryWallet, parseFloat(rewardAmount)); + } catch (error) { + console.warn('Transaction verification failed:', error.message); + // Continue processing but log the warning + } + + // Create or update issue record + const issue = await prisma.issue.upsert({ + where: { + id: issueId || `github-${Date.now()}`, // Generate ID if not provided + }, + update: { + title: issueTitle, + githubUrl: githubIssueUrl, + rewardAmount: parseFloat(rewardAmount), + tokenType, + tokenMintAddress, + status: 'FUNDED', + organizationWallet, + treasuryWallet, + transactionSignature, + updatedAt: new Date(), + }, + create: { + id: issueId || `github-${Date.now()}`, + title: issueTitle, + githubUrl: githubIssueUrl, + rewardAmount: parseFloat(rewardAmount), + tokenType, + tokenMintAddress, + status: 'FUNDED', + organizationWallet, + treasuryWallet, + transactionSignature, + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + + // Log the approval for audit purposes + console.log(`βœ… Reward approved for issue ${issue.id}:`, { + amount: rewardAmount, + tokenType, + signature: transactionSignature.slice(0, 16) + '...', + organization: organizationWallet.slice(0, 8) + '...', + }); + + return NextResponse.json({ + success: true, + issueId: issue.id, + message: 'Reward approved and recorded successfully', + issue: { + id: issue.id, + title: issue.title, + rewardAmount: issue.rewardAmount, + tokenType: issue.tokenType, + status: issue.status, + }, + }); + + } catch (error) { + console.error('Error processing organization approval:', error); + + return NextResponse.json( + { + error: 'Internal server error', + message: error.message, + }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} + +export async function GET(request) { + try { + // Get organization approvals/rewards + const { searchParams } = new URL(request.url); + const organizationWallet = searchParams.get('wallet'); + const status = searchParams.get('status'); + + let whereClause = {}; + + if (organizationWallet) { + whereClause.organizationWallet = organizationWallet; + } + + if (status) { + whereClause.status = status; + } + + const issues = await prisma.issue.findMany({ + where: whereClause, + orderBy: { + createdAt: 'desc', + }, + take: 50, // Limit to 50 most recent + }); + + return NextResponse.json({ + success: true, + issues, + count: issues.length, + }); + + } catch (error) { + console.error('Error fetching organization approvals:', error); + + return NextResponse.json( + { + error: 'Internal server error', + message: error.message, + }, + { status: 500 } + ); + } finally { + await prisma.$disconnect(); + } +} \ No newline at end of file diff --git a/app/eos-rewards/page.jsx b/app/eos-rewards/page.jsx new file mode 100644 index 0000000..abf8ca2 --- /dev/null +++ b/app/eos-rewards/page.jsx @@ -0,0 +1,278 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { WalletProvider } from '@solana/wallet-adapter-react'; +import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; +import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets'; +import { clusterApiUrl } from '@solana/web3.js'; +import OrganizationApproval from '@/components/OrganizationApproval'; +import { eosUtils, treasuryUtils } from '@/lib/solana-utils'; +import { fetchTokenMetadata } from '@/EOS/eostoken'; +import ClientOnly from '@/components/ClientOnly'; + +import '@solana/wallet-adapter-react-ui/styles.css'; + +const EosRewardDemo = () => { + const [metadata, setMetadata] = useState(null); + const [treasuryBalance, setTreasuryBalance] = useState(null); + const [loading, setLoading] = useState(true); + + const wallets = [new PhantomWalletAdapter()]; + const endpoint = clusterApiUrl('devnet'); + + useEffect(() => { + loadEosData(); + }, []); + + const loadEosData = async () => { + try { + setLoading(true); + const meta = await fetchTokenMetadata(); + setMetadata(meta); + + const treasuryBalanceData = await treasuryUtils.getEosBalance(); + setTreasuryBalance(treasuryBalanceData); + } catch (error) { + console.error('Error loading EOS data:', error); + } finally { + setLoading(false); + } + }; + + const handleApprovalComplete = (result) => { + console.log('EOS reward approval completed:', result); + loadEosData(); + }; + + return ( + +
+
+

+ Loading EOS Rewards... +

+
+
+ + }> + + +
+
+ {/* Header */} +
+
+ {metadata?.image && ( + {metadata.symbol} + )} +
+

+ {metadata?.name || 'EOS'} Hackathon Rewards +

+
+

+ πŸͺ™ {eosUtils.getMintAddress().slice(0, 16)}... +

+
+
+
+

+ πŸš€ Create and fund hackathon issues with EOS tokens on Solana devnet. + Contributors receive rewards automatically upon task completion. +

+
+ + {/* Stats Bar */} +
+
+
πŸ’΅
+
+ ${eosUtils.getPrice().toFixed(3)} +
+
USD per EOS
+
+ +
+
🏦
+
+ {treasuryBalance ? treasuryBalance.balance.toFixed(2) : '--'} +
+
Treasury EOS Balance
+
+ +
+
πŸ’°
+
+ ${treasuryBalance ? treasuryBalance.usdValue.toFixed(2) : '--'} +
+
Treasury USD Value
+
+ +
+
🌐
+
+ Devnet +
+
Solana Network
+
+
+ +
+ {/* Organization Flow */} +
+

+ 🏒 + Organization Portal +

+ +
+ + {/* Process Flow */} +
+

+ ⚑ + How It Works +

+
+
+
+ 1 +
+
+

Fund with EOS

+

+ Organizations transfer EOS tokens to our secure treasury for specific GitHub issues +

+
+
+ +
+
+ 2 +
+
+

Developer Contributes

+

+ Developers browse funded issues, work on solutions, and submit pull requests +

+
+
+ +
+
+ 3 +
+
+

Instant Reward

+

+ EOS tokens are automatically sent to contributor's wallet upon PR merge +

+
+
+
+ + {/* EOS Token Info */} +
+

+ πŸͺ™ + EOS Token Details +

+
+
+
Symbol
+
{metadata?.symbol || 'EOS'}
+
+
+
Decimals
+
{eosUtils.getDecimals()}
+
+
+
Network
+
Solana Devnet
+
+
+
Standard
+
SPL Token
+
+
+
+
+
+ + {/* Features Grid */} +
+
+
πŸ”’
+

Security First

+
    +
  • βœ“ Rate limiting protection
  • +
  • βœ“ Amount validation (1-1000 EOS)
  • +
  • βœ“ Transaction verification
  • +
  • βœ“ Fraud detection
  • +
  • βœ“ Real-time treasury monitoring
  • +
+
+ +
+
πŸš€
+

Powerful Features

+
    +
  • ⚑ Instant token transfers
  • +
  • πŸ”— GitHub integration
  • +
  • πŸ€– Automatic verification
  • +
  • πŸ“Š Real-time balance tracking
  • +
  • πŸ’Ό Multi-wallet support
  • +
+
+ +
+
πŸ”§
+

Built With

+
    +
  • ⚿ Solana blockchain
  • +
  • πŸͺ™ SPL token standard
  • +
  • 🌐 Devnet for testing
  • +
  • πŸ‘» Phantom wallet
  • +
  • βš›οΈ TypeScript & React
  • +
+
+
+ + {/* Call to Action */} +
+

Ready to Start Rewarding Contributors?

+

+ Join the future of hackathon rewards with EOS tokens on Solana +

+
+
+
{treasuryBalance ? treasuryBalance.balance.toFixed(0) : '0'}
+
EOS Ready to Distribute
+
+
+
$0.05
+
Per EOS Token
+
+
+
∞
+
Potential Impact
+
+
+
+
+
+
+
+
+ ); +}; + +export default EosRewardDemo; \ No newline at end of file diff --git a/app/layout.js b/app/layout.js index 3a47e8d..9001eac 100644 --- a/app/layout.js +++ b/app/layout.js @@ -22,14 +22,15 @@ export const metadata = { export default function RootLayout({ children }) { return ( - + {children} diff --git a/app/solana-demo/page.jsx b/app/solana-demo/page.jsx new file mode 100644 index 0000000..2d18d9f --- /dev/null +++ b/app/solana-demo/page.jsx @@ -0,0 +1,173 @@ +"use client"; + +import React from 'react'; +import { WalletProvider } from '@solana/wallet-adapter-react'; +import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; +import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets'; +import { clusterApiUrl } from '@solana/web3.js'; +import OrganizationApproval from '@/components/OrganizationApproval'; + +// Import wallet adapter CSS +import '@solana/wallet-adapter-react-ui/styles.css'; + +const SolanaRewardDemo = () => { + // Configure wallets + const wallets = [ + new PhantomWalletAdapter(), + ]; + + const endpoint = clusterApiUrl('devnet'); // Use devnet for testing + + const handleApprovalComplete = (result) => { + console.log('Reward approval completed:', result); + // Handle the completion (e.g., redirect, show success message, etc.) + }; + + return ( + + +
+
+
+

+ Solana Hackathon Reward System +

+

+ Create and fund hackathon issues with SOL or SPL tokens. + Contributors receive rewards automatically upon task completion. +

+
+ +
+ {/* Organization Flow */} +
+

+ Organization: Fund Issue +

+ +
+ + {/* Flow Explanation */} +
+

How It Works

+
+
+
+ 1 +
+
+

Create & Fund Issue

+

+ Organization specifies reward amount and approves token transfer to treasury +

+
+
+ +
+
+ 2 +
+
+

Contributor Works

+

+ Contributors see funded issues and work on solving them +

+
+
+ +
+
+ 3 +
+
+

Automatic Reward

+

+ Upon PR merge, system automatically sends reward to contributor's wallet +

+
+
+
+ +
+

Security Features

+
    +
  • β€’ Rate limiting (10 rewards/hour)
  • +
  • β€’ Transaction verification
  • +
  • β€’ Real-time treasury monitoring
  • +
  • β€’ Fraud detection algorithms
  • +
+
+
+
+ + {/* Technical Details */} +
+

Technical Implementation

+
+
+

Supported Tokens

+
    +
  • β€’ SOL (Native Solana)
  • +
  • β€’ SPL Tokens
  • +
  • β€’ Custom token mints
  • +
+
+ +
+

Supported Wallets

+
    +
  • β€’ Phantom
  • +
  • β€’ Solflare
  • +
  • β€’ Other Solana wallets
  • +
+
+ +
+

Network

+
    +
  • β€’ Devnet (Testing)
  • +
  • β€’ Mainnet (Production)
  • +
  • β€’ Custom RPC endpoints
  • +
+
+
+
+ + {/* API Examples */} +
+

Integration Examples

+
+
+

Environment Variables

+
+{`SOLANA_RPC_URL=https://api.devnet.solana.com
+TREASURY_PRIVATE_KEY=[Your_Treasury_Private_Key]
+NEXT_PUBLIC_TREASURY_WALLET=[Treasury_Public_Key]`}
+                  
+
+ +
+

Basic Usage

+
+{`import { rewardContributorForIssue } from '@/actions/rewardContributorForIssue';
+
+// Reward a contributor
+const result = await rewardContributorForIssue(
+  githubIssueId,
+  githubUsername
+);`}
+                  
+
+
+
+
+
+
+
+ ); +}; + +export default SolanaRewardDemo; \ No newline at end of file diff --git a/app/test-inputs/page.jsx b/app/test-inputs/page.jsx new file mode 100644 index 0000000..ee21e94 --- /dev/null +++ b/app/test-inputs/page.jsx @@ -0,0 +1,66 @@ +'use client'; + +import OrganizationApproval from '@/components/OrganizationApproval'; + +export default function TestInputsPage() { + return ( +
+
+

+ πŸ§ͺ Input Visibility Test +

+ + {/* Test standard inputs */} +
+

Standard HTML Inputs

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Organization Approval Component */} +
+

Organization Approval Component

+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/ClientOnly.jsx b/components/ClientOnly.jsx new file mode 100644 index 0000000..8f7b5b9 --- /dev/null +++ b/components/ClientOnly.jsx @@ -0,0 +1,17 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +export default function ClientOnly({ children, fallback = null }) { + const [hasMounted, setHasMounted] = useState(false); + + useEffect(() => { + setHasMounted(true); + }, []); + + if (!hasMounted) { + return fallback; + } + + return children; +} \ No newline at end of file diff --git a/components/OrganizationApproval.jsx b/components/OrganizationApproval.jsx new file mode 100644 index 0000000..449ca4f --- /dev/null +++ b/components/OrganizationApproval.jsx @@ -0,0 +1,526 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useWallet } from "@solana/wallet-adapter-react"; +import { WalletMultiButton } from "@solana/wallet-adapter-react-ui"; +import { + createSolTransferTransaction, + createTokenTransferTransaction, + getSolBalance, + getTokenBalance, + getEosBalance, + treasuryUtils, + eosUtils, + isValidWalletAddress +} from "@/lib/solana-utils"; +import { Connection } from "@solana/web3.js"; +import ClientOnly from './ClientOnly'; + +const OrganizationApproval = ({ issueId, onApprovalComplete }) => { + const { connected, publicKey, sendTransaction } = useWallet(); + const [loading, setLoading] = useState(false); + const [step, setStep] = useState(1); + + // Initialize connection + const connection = new Connection("https://api.devnet.solana.com", "confirmed"); + + const [formData, setFormData] = useState({ + rewardAmount: "", + tokenType: "EOS", // Default to EOS token + tokenMintAddress: eosUtils.getMintAddress(), // EOS token mint + issueTitle: "", + githubIssueUrl: "", + }); + const [walletBalance, setWalletBalance] = useState(0); + const [treasuryAddress, setTreasuryAddress] = useState(""); + + useEffect(() => { + // Get treasury address from environment + setTreasuryAddress(treasuryUtils.getPublicKey().toString()); + }, []); + + useEffect(() => { + if (connected && publicKey) { + fetchWalletBalance(); + } + }, [connected, publicKey, formData.tokenType, formData.tokenMintAddress]); + + const fetchWalletBalance = async () => { + try { + if (formData.tokenType === "EOS") { + const balanceData = await getEosBalance(publicKey.toString()); + setWalletBalance(balanceData.balance); + } else if (formData.tokenType === "SOL") { + const balance = await getSolBalance(publicKey.toString()); + setWalletBalance(balance); + } else if (formData.tokenType === "SPL" && formData.tokenMintAddress) { + const balance = await getTokenBalance(publicKey.toString(), formData.tokenMintAddress); + setWalletBalance(balance); + } + } catch (error) { + console.error("Error fetching wallet balance:", error); + console.error("Failed to fetch wallet balance"); + } + }; + + const handleFormSubmit = (e) => { + e.preventDefault(); + + // Validation + if (!formData.rewardAmount || parseFloat(formData.rewardAmount) <= 0) { + alert("Please enter a valid reward amount"); + return; + } + + if (formData.tokenType === "SPL" && !isValidWalletAddress(formData.tokenMintAddress)) { + alert("Please enter a valid token mint address"); + return; + } + + if (parseFloat(formData.rewardAmount) > walletBalance) { + alert("Insufficient balance for the reward amount"); + return; + } + + setStep(2); + }; + + const handleApproveTransfer = async () => { + if (!connected || !publicKey) { + alert("Please connect your wallet first"); + return; + } + + setLoading(true); + + try { + let transaction; + const amount = parseFloat(formData.rewardAmount); + + console.log('Starting transaction approval process:', { + amount, + tokenType: formData.tokenType, + from: publicKey.toString().slice(0, 8) + '...', + to: treasuryAddress.slice(0, 8) + '...' + }); + + if (formData.tokenType === "EOS") { + // For EOS token, check if token account exists first + const userBalance = await getEosBalance(publicKey.toString()); + console.log('User EOS balance:', userBalance); + + if (userBalance < amount) { + throw new Error(`Insufficient EOS balance. You have ${userBalance} EOS but trying to transfer ${amount} EOS`); + } + + // Use SPL token transfer for EOS + transaction = await createTokenTransferTransaction( + publicKey.toString(), + treasuryAddress, + eosUtils.getMintAddress(), + Math.floor(amount * (10 ** eosUtils.getDecimals())) // Convert to token units + ); + } else if (formData.tokenType === "SOL") { + // Check SOL balance + const userBalance = await getSolBalance(publicKey.toString()); + console.log('User SOL balance:', userBalance); + + if (userBalance < amount + 0.01) { // Include fee + throw new Error(`Insufficient SOL balance. You have ${userBalance} SOL but trying to transfer ${amount} SOL plus fees`); + } + + transaction = await createSolTransferTransaction( + publicKey.toString(), + treasuryAddress, + amount + ); + } else { + // For other SPL tokens + const userBalance = await getTokenBalance(publicKey.toString(), formData.tokenMintAddress); + console.log('User token balance:', userBalance); + + if (userBalance < amount) { + throw new Error(`Insufficient token balance. You have ${userBalance} tokens but trying to transfer ${amount} tokens`); + } + + transaction = await createTokenTransferTransaction( + publicKey.toString(), + treasuryAddress, + formData.tokenMintAddress, + amount + ); + } + + console.log('Transaction created, requesting user approval...'); + + // Send transaction for user approval + const signature = await sendTransaction(transaction, connection, { + skipPreflight: false, + preflightCommitment: 'processed' + }); + + console.log('Transaction signed, waiting for confirmation:', signature); + + // Wait for confirmation + await connection.confirmTransaction(signature, "confirmed"); + + console.log('Transaction confirmed, saving to database...'); + + // Save to database via API call + const response = await fetch("/api/organization/approve-reward", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + issueId, + rewardAmount: amount, + tokenType: formData.tokenType, + tokenMintAddress: formData.tokenMintAddress || null, + transactionSignature: signature, + organizationWallet: publicKey.toString(), + treasuryWallet: treasuryAddress, + issueTitle: formData.issueTitle, + githubIssueUrl: formData.githubIssueUrl, + }), + }); + + if (!response.ok) { + throw new Error("Failed to save approval to database"); + } + + const result = await response.json(); + + alert("Reward approved and transferred to treasury!"); + setStep(3); + + if (onApprovalComplete) { + onApprovalComplete({ + signature, + amount, + tokenType: formData.tokenType, + issueId: result.issueId, + }); + } + + } catch (error) { + console.error("Error approving transfer:", error); + + let errorMessage = "Failed to approve transfer"; + + // Provide specific error messages for common issues + if (error.message?.includes('insufficient')) { + errorMessage = error.message; + } else if (error.message?.includes('User rejected')) { + errorMessage = "Transaction was cancelled by user"; + } else if (error.message?.includes('TokenAccountNotFoundError')) { + errorMessage = "Token account not found. You may need to create a token account first."; + } else if (error.message?.includes('Unexpected error')) { + errorMessage = "Wallet error occurred. Please try again or check your wallet connection."; + } else if (error.message) { + errorMessage = error.message; + } + + alert(errorMessage); + } finally { + setLoading(false); + } + }; + + const resetForm = () => { + setStep(1); + setFormData({ + rewardAmount: "", + tokenType: "SOL", + tokenMintAddress: "", + issueTitle: "", + githubIssueUrl: "", + }); + }; + + return ( +
+

+ 🏒 Approve EOS Reward +

+ + {/* Debug Info */} +
+ Step: {step} | Connected: {connected ? 'Yes' : 'No'} | Balance: {walletBalance} +
+ + {/* Step 1: Setup Reward */} + {step === 1 && ( +
+
+ + setFormData(prev => ({ ...prev, issueTitle: e.target.value }))} + placeholder="Enter issue title" + required + className="w-full p-4 border-2 border-gray-400 rounded-xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all bg-white text-gray-900" + style={{ minHeight: '50px', fontSize: '16px' }} + /> +
+ +
+ + setFormData(prev => ({ ...prev, githubIssueUrl: e.target.value }))} + placeholder="https://github.com/owner/repo/issues/123" + required + className="w-full p-4 border-2 border-gray-400 rounded-xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all bg-white text-gray-900" + style={{ minHeight: '50px', fontSize: '16px' }} + /> +
+ +
+ + +
+ + {formData.tokenType === "SPL" && ( +
+ + setFormData(prev => ({ ...prev, tokenMintAddress: e.target.value }))} + placeholder="Enter SPL token mint address" + required + className="w-full p-4 border-2 border-gray-400 rounded-xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all bg-white text-gray-900 font-mono text-sm" + style={{ minHeight: '50px' }} + /> +
+ )} + +
+ + setFormData(prev => ({ ...prev, rewardAmount: e.target.value }))} + placeholder="Enter amount" + required + className="w-full p-4 border-2 border-gray-400 rounded-xl focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all bg-white text-gray-900 text-lg font-semibold" + style={{ minHeight: '50px', fontSize: '16px' }} + /> +
+

+ πŸ’³ Available: {walletBalance} {formData.tokenType === "EOS" ? "EOS" : formData.tokenType === "SOL" ? "SOL" : "tokens"} + {formData.tokenType === "EOS" && ( + + (${(walletBalance * eosUtils.getPrice()).toFixed(2)} USD) + + )} +

+
+
+ +
+

+ 🏦 Treasury Address: +

+

+ {treasuryAddress} +

+
+ + {!connected ? ( +
+
+

⚠️ Connect your wallet to continue

+
+ + Loading Wallet... + + }> + + +
+ ) : ( + + )} +
+ )} + + {/* Step 2: Approve Transfer */} + {step === 2 && ( +
+

+ πŸ” Review & Approve +

+ +
+
+ πŸ“ Issue: + {formData.issueTitle} +
+
+ πŸ’° Amount: + + {formData.rewardAmount} {formData.tokenType} + {formData.tokenType === "EOS" && ( +
+ β‰ˆ ${(parseFloat(formData.rewardAmount) * eosUtils.getPrice()).toFixed(2)} USD +
+ )} +
+
+
+ πŸ“€ From: + + {publicKey?.toString().slice(0, 8)}...{publicKey?.toString().slice(-8)} + +
+
+ 🏦 To Treasury: + + {treasuryAddress.slice(0, 8)}...{treasuryAddress.slice(-8)} + +
+
+ +
+
+ ⚠️ +
+

Important Notice

+

+ This will transfer {formData.rewardAmount} {formData.tokenType} to our treasury. + The funds will be automatically released to contributors when they complete the issue. +

+
+
+
+ +
+ + +
+
+ )} + + {/* Step 3: Complete */} + {step === 3 && ( +
+
+
πŸŽ‰
+

+ Success! Reward Approved +

+

+ Your EOS tokens have been transferred to the treasury! +

+
+ +
+

πŸ“Š Transaction Summary

+
+
+ πŸ’° Amount: + + {formData.rewardAmount} {formData.tokenType} + {formData.tokenType === "EOS" && ( + + (${(parseFloat(formData.rewardAmount) * eosUtils.getPrice()).toFixed(2)} USD) + + )} + +
+
+ πŸ“ Issue: + {formData.issueTitle} +
+
+ πŸ”— URL: + + View Issue β†’ + +
+
+
+ +
+

+ πŸš€ Next Steps: Contributors can now work on this issue. + Once they submit a successful pull request, they'll automatically receive + the {formData.rewardAmount} {formData.tokenType} reward! +

+
+ + +
+ )} +
+ ); +}; + +export default OrganizationApproval; diff --git a/components/theme-provider.jsx b/components/theme-provider.jsx index 580e8c1..916a3aa 100644 --- a/components/theme-provider.jsx +++ b/components/theme-provider.jsx @@ -4,5 +4,15 @@ import * as React from "react"; import { ThemeProvider as NextThemesProvider } from "next-themes"; export function ThemeProvider({ children, ...props }) { + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return children; + } + return {children}; } diff --git a/lib/security-utils.js b/lib/security-utils.js new file mode 100644 index 0000000..24c875a --- /dev/null +++ b/lib/security-utils.js @@ -0,0 +1,351 @@ +import { connection, verifyTransaction, getTransactionDetails } from "./solana-utils"; +import { prisma } from "./prisma"; + +/** + * Security utilities for Solana reward system + */ + +// Rate limiting configuration +const RATE_LIMITS = { + REWARD_REQUESTS_PER_HOUR: 10, + MAX_REWARD_AMOUNT_SOL: 10, // Maximum reward per issue in SOL + MIN_REWARD_AMOUNT_SOL: 0.001, // Minimum reward per issue in SOL + MAX_REWARD_AMOUNT_EOS: 1000, // Maximum reward per issue in EOS + MIN_REWARD_AMOUNT_EOS: 1, // Minimum reward per issue in EOS +}; + +// Suspicious activity patterns +const SECURITY_CHECKS = { + MAX_FAILED_TRANSACTIONS: 5, + TRANSACTION_TIMEOUT_MS: 30000, // 30 seconds + MINIMUM_CONFIRMATION_TIME: 1000, // 1 second +}; + +/** + * Verify that a user hasn't exceeded rate limits + * @param {string} userId - User ID to check + * @param {string} type - Type of action (e.g., 'reward_request') + * @returns {Promise<{allowed: boolean, remaining: number}>} + */ +export async function checkRateLimit(userId, type = 'reward_request') { + try { + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + + const recentRequests = await prisma.contribution.count({ + where: { + contributorId: userId, + completedAt: { + gte: oneHourAgo, + }, + }, + }); + + const allowed = recentRequests < RATE_LIMITS.REWARD_REQUESTS_PER_HOUR; + const remaining = Math.max(0, RATE_LIMITS.REWARD_REQUESTS_PER_HOUR - recentRequests); + + return { allowed, remaining }; + } catch (error) { + console.error("Error checking rate limit:", error); + return { allowed: false, remaining: 0 }; + } +} + +/** + * Validate reward amount against security limits + * @param {number} amount - Reward amount + * @param {string} tokenType - Token type ("SOL" or "SPL") + * @returns {Promise<{valid: boolean, reason?: string}>} + */ +export async function validateRewardAmount(amount, tokenType = "EOS") { + try { + if (tokenType === "SOL") { + if (amount < RATE_LIMITS.MIN_REWARD_AMOUNT_SOL) { + return { + valid: false, + reason: `Minimum reward amount is ${RATE_LIMITS.MIN_REWARD_AMOUNT_SOL} SOL` + }; + } + + if (amount > RATE_LIMITS.MAX_REWARD_AMOUNT_SOL) { + return { + valid: false, + reason: `Maximum reward amount is ${RATE_LIMITS.MAX_REWARD_AMOUNT_SOL} SOL` + }; + } + } else if (tokenType === "EOS") { + if (amount < RATE_LIMITS.MIN_REWARD_AMOUNT_EOS) { + return { + valid: false, + reason: `Minimum reward amount is ${RATE_LIMITS.MIN_REWARD_AMOUNT_EOS} EOS` + }; + } + + if (amount > RATE_LIMITS.MAX_REWARD_AMOUNT_EOS) { + return { + valid: false, + reason: `Maximum reward amount is ${RATE_LIMITS.MAX_REWARD_AMOUNT_EOS} EOS` + }; + } + } + + return { valid: true }; + } catch (error) { + console.error("Error validating reward amount:", error); + return { valid: false, reason: "Validation error" }; + } +} + +/** + * Verify transaction integrity and detect potential fraud + * @param {string} signature - Transaction signature + * @param {string} expectedRecipient - Expected recipient address + * @param {number} expectedAmount - Expected amount + * @returns {Promise<{valid: boolean, reason?: string, details?: object}>} + */ +export async function verifyTransactionIntegrity(signature, expectedRecipient, expectedAmount) { + try { + // Basic transaction existence check + const exists = await verifyTransaction(signature); + if (!exists) { + return { valid: false, reason: "Transaction not found or not confirmed" }; + } + + // Get detailed transaction information + const details = await getTransactionDetails(signature); + if (!details) { + return { valid: false, reason: "Unable to retrieve transaction details" }; + } + + // Check transaction timing (prevent pre-computed transactions) + const transactionTime = new Date(details.blockTime * 1000); + const now = new Date(); + const timeDiff = now - transactionTime; + + if (timeDiff < SECURITY_CHECKS.MINIMUM_CONFIRMATION_TIME) { + return { + valid: false, + reason: "Transaction confirmed too quickly - potential fraud" + }; + } + + if (timeDiff > SECURITY_CHECKS.TRANSACTION_TIMEOUT_MS) { + return { + valid: false, + reason: "Transaction is too old" + }; + } + + // Additional checks can be added here: + // - Verify the actual transfer amount matches expected + // - Check for proper token program interactions + // - Validate source and destination addresses + + return { + valid: true, + details: { + blockTime: transactionTime, + confirmations: details.meta?.confirmations || 0, + fee: details.meta?.fee || 0, + } + }; + + } catch (error) { + console.error("Error verifying transaction integrity:", error); + return { valid: false, reason: "Transaction verification failed" }; + } +} + +/** + * Real-time fund verification for treasury wallet + * @param {string} tokenType - "SOL" or "SPL" + * @param {string} tokenMintAddress - Token mint address (for SPL tokens) + * @returns {Promise<{hasFunds: boolean, balance: number, reason?: string}>} + */ +export async function verifyTreasuryFunds(tokenType = "EOS", tokenMintAddress = null) { + try { + const { treasuryUtils } = await import("./solana-utils"); + + let balance; + + if (tokenType === "SOL") { + balance = await treasuryUtils.getSolBalance(); + } else if (tokenType === "EOS") { + const balanceData = await treasuryUtils.getEosBalance(); + balance = balanceData.balance; + } else if (tokenType === "SPL" && tokenMintAddress) { + balance = await treasuryUtils.getTokenBalance(tokenMintAddress); + } else { + return { hasFunds: false, balance: 0, reason: "Invalid token configuration" }; + } + + // Minimum balance thresholds + const minimumBalance = tokenType === "SOL" ? 0.1 : tokenType === "EOS" ? 10 : 100; + const hasFunds = balance >= minimumBalance; + + return { + hasFunds, + balance, + reason: hasFunds ? null : `Insufficient treasury balance: ${balance} ${tokenType}` + }; + + } catch (error) { + console.error("Error verifying treasury funds:", error); + return { hasFunds: false, balance: 0, reason: "Treasury verification failed" }; + } +} + +/** + * Log security events for monitoring + * @param {string} event - Event type + * @param {object} data - Event data + * @param {string} severity - Severity level ("info", "warning", "error") + */ +export async function logSecurityEvent(event, data, severity = "info") { + try { + const logEntry = { + timestamp: new Date().toISOString(), + event, + severity, + data, + }; + + console.log(`[SECURITY ${severity.toUpperCase()}] ${event}:`, data); + + // In production, you might want to send this to a monitoring service + // await sendToMonitoringService(logEntry); + + } catch (error) { + console.error("Error logging security event:", error); + } +} + +/** + * Comprehensive security check before processing rewards + * @param {object} params - Check parameters + * @returns {Promise<{passed: boolean, failures: string[]}>} + */ +export async function performSecurityChecks({ + userId, + walletAddress, + rewardAmount, + tokenType, + tokenMintAddress, + transactionSignature, +}) { + const failures = []; + + try { + // Rate limiting check + const rateLimitCheck = await checkRateLimit(userId); + if (!rateLimitCheck.allowed) { + failures.push(`Rate limit exceeded. Remaining: ${rateLimitCheck.remaining}`); + } + + // Reward amount validation + const amountCheck = await validateRewardAmount(rewardAmount, tokenType); + if (!amountCheck.valid) { + failures.push(amountCheck.reason); + } + + // Transaction integrity check + if (transactionSignature) { + const integrityCheck = await verifyTransactionIntegrity( + transactionSignature, + walletAddress, + rewardAmount + ); + if (!integrityCheck.valid) { + failures.push(`Transaction integrity: ${integrityCheck.reason}`); + } + } + + // Treasury funds verification + const fundsCheck = await verifyTreasuryFunds(tokenType, tokenMintAddress); + if (!fundsCheck.hasFunds) { + failures.push(fundsCheck.reason); + } + + // Log security check result + await logSecurityEvent("security_check", { + userId, + walletAddress, + rewardAmount, + tokenType, + passed: failures.length === 0, + failures, + }, failures.length > 0 ? "warning" : "info"); + + return { passed: failures.length === 0, failures }; + + } catch (error) { + console.error("Error performing security checks:", error); + failures.push("Security check system error"); + + await logSecurityEvent("security_check_error", { + userId, + error: error.message, + }, "error"); + + return { passed: false, failures }; + } +} + +/** + * Monitor for suspicious patterns in real-time + * @param {string} userId - User ID to monitor + * @returns {Promise<{suspicious: boolean, patterns: string[]}>} + */ +export async function detectSuspiciousActivity(userId) { + const patterns = []; + + try { + // Check for rapid successive reward claims + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); + const recentClaims = await prisma.contribution.count({ + where: { + contributorId: userId, + completedAt: { + gte: fiveMinutesAgo, + }, + }, + }); + + if (recentClaims > 3) { + patterns.push("Rapid successive reward claims"); + } + + // Check for failed transactions pattern + // This would require additional tracking of failed attempts + + // Check for unusual wallet patterns + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { walletAddress: true, createdAt: true }, + }); + + if (user) { + const accountAge = Date.now() - user.createdAt.getTime(); + const oneDay = 24 * 60 * 60 * 1000; + + if (accountAge < oneDay && recentClaims > 0) { + patterns.push("New account with immediate reward claims"); + } + } + + return { suspicious: patterns.length > 0, patterns }; + + } catch (error) { + console.error("Error detecting suspicious activity:", error); + return { suspicious: false, patterns: [] }; + } +} + +export default { + checkRateLimit, + validateRewardAmount, + verifyTransactionIntegrity, + verifyTreasuryFunds, + logSecurityEvent, + performSecurityChecks, + detectSuspiciousActivity, +}; \ No newline at end of file diff --git a/lib/solana-utils.js b/lib/solana-utils.js new file mode 100644 index 0000000..068c7e1 --- /dev/null +++ b/lib/solana-utils.js @@ -0,0 +1,475 @@ +import { + Connection, + PublicKey, + Transaction, + SystemProgram, + LAMPORTS_PER_SOL, + Keypair, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + createTransferInstruction, + getAssociatedTokenAddress, + createAssociatedTokenAccountInstruction, + getAccount, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; + +// Solana connection configuration (Devnet for EOS token) +const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL || "https://api.devnet.solana.com"; +const TREASURY_PRIVATE_KEY = process.env.TREASURY_PRIVATE_KEY; // Base58 encoded private key +const TREASURY_PUBLIC_KEY = process.env.NEXT_PUBLIC_TREASURY_WALLET; + +// EOS Token Configuration +const EOS_TOKEN_MINT = "FVWUJ8Ut6kT2fSM6bHkGGTJ32FmjQ2VGvyLwSzBAknA8"; +const EOS_DECIMALS = 6; +const EOS_PRICE_USD = 0.05; + +// Initialize Solana connection +export const connection = new Connection(SOLANA_RPC_URL, "confirmed"); + +// Create treasury wallet from private key +export function getTreasuryWallet() { + if (!TREASURY_PRIVATE_KEY) { + throw new Error("Treasury private key not found in environment variables"); + } + return Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(TREASURY_PRIVATE_KEY)) + ); +} + +/** + * Get SOL balance of a wallet + * @param {string} walletAddress - Wallet address to check balance + * @returns {Promise} Balance in SOL + */ +export async function getSolBalance(walletAddress) { + try { + const publicKey = new PublicKey(walletAddress); + const balance = await connection.getBalance(publicKey); + return balance / LAMPORTS_PER_SOL; + } catch (error) { + console.error("Error getting SOL balance:", error); + throw new Error("Failed to get SOL balance"); + } +} + +/** + * Get SPL token balance of a wallet + * @param {string} walletAddress - Wallet address to check balance + * @param {string} tokenMintAddress - Token mint address + * @returns {Promise} Token balance + */ +export async function getTokenBalance(walletAddress, tokenMintAddress) { + try { + const publicKey = new PublicKey(walletAddress); + const mintKey = new PublicKey(tokenMintAddress); + + const associatedTokenAddress = await getAssociatedTokenAddress( + mintKey, + publicKey + ); + + // Check if account exists first + const accountInfo = await connection.getAccountInfo(associatedTokenAddress); + if (!accountInfo) { + console.log(`Token account does not exist for wallet: ${walletAddress.slice(0, 8)}...`); + return 0; + } + + const account = await getAccount(connection, associatedTokenAddress); + return Number(account.amount); + } catch (error) { + console.error("Error getting token balance:", error); + // If it's a TokenAccountNotFoundError, return 0 + if (error.name === 'TokenAccountNotFoundError' || + error.message?.includes('TokenAccountNotFoundError') || + error.message?.includes('could not find account')) { + console.log(`Token account not found, returning 0 balance for ${walletAddress.slice(0, 8)}...`); + return 0; + } + return 0; + } +} + +/** + * Create a transaction for SOL transfer approval + * @param {string} fromAddress - Sender's wallet address + * @param {string} toAddress - Recipient's wallet address (treasury) + * @param {number} amount - Amount in SOL + * @returns {Promise} Unsigned transaction + */ +export async function createSolTransferTransaction(fromAddress, toAddress, amount) { + try { + const fromPubkey = new PublicKey(fromAddress); + const toPubkey = new PublicKey(toAddress); + const lamports = amount * LAMPORTS_PER_SOL; + + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey, + toPubkey, + lamports, + }) + ); + + // Get recent blockhash + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = fromPubkey; + + return transaction; + } catch (error) { + console.error("Error creating SOL transfer transaction:", error); + throw new Error("Failed to create SOL transfer transaction"); + } +} + +/** + * Create a transaction for SPL token transfer approval + * @param {string} fromAddress - Sender's wallet address + * @param {string} toAddress - Recipient's wallet address (treasury) + * @param {string} tokenMintAddress - Token mint address + * @param {number} amount - Amount of tokens + * @returns {Promise} Unsigned transaction + */ +export async function createTokenTransferTransaction( + fromAddress, + toAddress, + tokenMintAddress, + amount +) { + try { + const fromPubkey = new PublicKey(fromAddress); + const toPubkey = new PublicKey(toAddress); + const mintKey = new PublicKey(tokenMintAddress); + + // Get associated token addresses + const fromTokenAccount = await getAssociatedTokenAddress( + mintKey, + fromPubkey + ); + const toTokenAccount = await getAssociatedTokenAddress( + mintKey, + toPubkey + ); + + const transaction = new Transaction(); + + // Check if recipient's token account exists, if not create it + try { + await getAccount(connection, toTokenAccount); + } catch (error) { + // Account doesn't exist, add instruction to create it + transaction.add( + createAssociatedTokenAccountInstruction( + fromPubkey, // payer + toTokenAccount, // associated token account + toPubkey, // owner + mintKey // mint + ) + ); + } + + // Add transfer instruction + transaction.add( + createTransferInstruction( + fromTokenAccount, // source + toTokenAccount, // destination + fromPubkey, // owner + amount // amount + ) + ); + + // Get recent blockhash + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = fromPubkey; + + return transaction; + } catch (error) { + console.error("Error creating token transfer transaction:", error); + throw new Error("Failed to create token transfer transaction"); + } +} + +/** + * Send SOL from treasury to user wallet + * @param {string} recipientAddress - Recipient's wallet address + * @param {number} amount - Amount in SOL + * @returns {Promise} Transaction signature + */ +export async function sendSolFromTreasury(recipientAddress, amount) { + try { + const treasuryWallet = getTreasuryWallet(); + const recipientPubkey = new PublicKey(recipientAddress); + const lamports = amount * LAMPORTS_PER_SOL; + + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: treasuryWallet.publicKey, + toPubkey: recipientPubkey, + lamports, + }) + ); + + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [treasuryWallet] + ); + + console.log(`SOL transfer successful: ${signature}`); + return signature; + } catch (error) { + console.error("Error sending SOL from treasury:", error); + throw new Error("Failed to send SOL from treasury"); + } +} + +/** + * Send SPL tokens from treasury to user wallet + * @param {string} recipientAddress - Recipient's wallet address + * @param {string} tokenMintAddress - Token mint address + * @param {number} amount - Amount of tokens + * @returns {Promise} Transaction signature + */ +export async function sendTokenFromTreasury( + recipientAddress, + tokenMintAddress, + amount +) { + try { + const treasuryWallet = getTreasuryWallet(); + const recipientPubkey = new PublicKey(recipientAddress); + const mintKey = new PublicKey(tokenMintAddress); + + // Get associated token addresses + const treasuryTokenAccount = await getAssociatedTokenAddress( + mintKey, + treasuryWallet.publicKey + ); + const recipientTokenAccount = await getAssociatedTokenAddress( + mintKey, + recipientPubkey + ); + + const transaction = new Transaction(); + + // Check if recipient's token account exists, if not create it + try { + await getAccount(connection, recipientTokenAccount); + } catch (error) { + // Account doesn't exist, add instruction to create it + transaction.add( + createAssociatedTokenAccountInstruction( + treasuryWallet.publicKey, // payer + recipientTokenAccount, // associated token account + recipientPubkey, // owner + mintKey // mint + ) + ); + } + + // Add transfer instruction + transaction.add( + createTransferInstruction( + treasuryTokenAccount, // source + recipientTokenAccount, // destination + treasuryWallet.publicKey, // owner + amount // amount + ) + ); + + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [treasuryWallet] + ); + + console.log(`Token transfer successful: ${signature}`); + return signature; + } catch (error) { + console.error("Error sending tokens from treasury:", error); + throw new Error("Failed to send tokens from treasury"); + } +} + +/** + * Verify if a transaction exists and is confirmed + * @param {string} signature - Transaction signature + * @returns {Promise} Whether transaction is confirmed + */ +export async function verifyTransaction(signature) { + try { + const transaction = await connection.getTransaction(signature, { + commitment: "confirmed", + }); + return transaction !== null; + } catch (error) { + console.error("Error verifying transaction:", error); + return false; + } +} + +/** + * Validate if a wallet address is valid + * @param {string} address - Wallet address to validate + * @returns {boolean} Whether address is valid + */ +export function isValidWalletAddress(address) { + try { + new PublicKey(address); + return true; + } catch (error) { + return false; + } +} + +/** + * Get transaction details + * @param {string} signature - Transaction signature + * @returns {Promise} Transaction details or null + */ +export async function getTransactionDetails(signature) { + try { + const transaction = await connection.getTransaction(signature, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }); + return transaction; + } catch (error) { + console.error("Error getting transaction details:", error); + return null; + } +} + +/** + * Get EOS token balance of a wallet + * @param {string} walletAddress - Wallet address to check balance + * @returns {Promise<{balance: number, usdValue: number}>} EOS balance and USD value + */ +export async function getEosBalance(walletAddress) { + try { + if (!walletAddress) { + return 0; + } + const balance = await getTokenBalance(walletAddress, eosUtils.getMintAddress()); + return balance || 0; + } catch (error) { + console.error('Error getting EOS balance:', error); + // If it's a token account not found error, return 0 + if (error.name === 'TokenAccountNotFoundError' || error.message.includes('TokenAccountNotFoundError')) { + return 0; + } + return 0; + } +} + +/** + * Send EOS tokens from treasury to user wallet + * @param {string} recipientAddress - Recipient's wallet address + * @param {number} amount - Amount of EOS tokens + * @returns {Promise} Transaction signature + */ +export async function sendEosFromTreasury(recipientAddress, amount) { + try { + const treasuryWallet = getTreasuryWallet(); + const recipientPubkey = new PublicKey(recipientAddress); + const mintKey = new PublicKey(EOS_TOKEN_MINT); + + // Convert amount to token units (multiply by decimals) + const tokenAmount = Math.floor(amount * (10 ** EOS_DECIMALS)); + + // Get associated token addresses + const treasuryTokenAccount = await getAssociatedTokenAddress( + mintKey, + treasuryWallet.publicKey + ); + const recipientTokenAccount = await getAssociatedTokenAddress( + mintKey, + recipientPubkey + ); + + const transaction = new Transaction(); + + // Check if recipient's token account exists, if not create it + try { + await getAccount(connection, recipientTokenAccount); + } catch (error) { + // Account doesn't exist, add instruction to create it + transaction.add( + createAssociatedTokenAccountInstruction( + treasuryWallet.publicKey, // payer + recipientTokenAccount, // associated token account + recipientPubkey, // owner + mintKey // mint + ) + ); + } + + // Add transfer instruction + transaction.add( + createTransferInstruction( + treasuryTokenAccount, // source + recipientTokenAccount, // destination + treasuryWallet.publicKey, // owner + tokenAmount // amount in token units + ) + ); + + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [treasuryWallet] + ); + + console.log(`EOS transfer successful: ${signature}`); + return signature; + } catch (error) { + console.error("Error sending EOS from treasury:", error); + throw new Error("Failed to send EOS from treasury"); + } +} + +// Treasury wallet utilities +export const treasuryUtils = { + getPublicKey: () => { + if (!TREASURY_PUBLIC_KEY) { + throw new Error("Treasury public key not found in environment variables"); + } + return new PublicKey(TREASURY_PUBLIC_KEY); + }, + + getSolBalance: async () => { + return await getSolBalance(TREASURY_PUBLIC_KEY); + }, + + getTokenBalance: async (tokenMintAddress) => { + return await getTokenBalance(TREASURY_PUBLIC_KEY, tokenMintAddress); + }, + + getEosBalance: async () => { + const balance = await getEosBalance(TREASURY_PUBLIC_KEY); + return { + balance: balance || 0, + usdValue: (balance || 0) * EOS_PRICE_USD, + }; + }, +}; + +// EOS Token utilities +export const eosUtils = { + getMintAddress: () => EOS_TOKEN_MINT, + getDecimals: () => EOS_DECIMALS, + getPrice: () => EOS_PRICE_USD, + + formatAmount: (amount) => { + return `${amount} EOS ($${(amount * EOS_PRICE_USD).toFixed(2)})`; + }, + + parseAmount: (tokenUnits) => { + return tokenUnits / (10 ** EOS_DECIMALS); + }, +}; diff --git a/package.json b/package.json index 35a19a5..698bf0f 100644 --- a/package.json +++ b/package.json @@ -21,27 +21,24 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - - "@clerk/nextjs": "^6.33.2", - "@prisma/client": "^6.16.3", "@solana/spl-token": "^0.4.14", "@solana/wallet-adapter-base": "^0.9.27", "@solana/wallet-adapter-react": "^0.15.39", "@solana/wallet-adapter-react-ui": "^0.9.39", "@solana/wallet-adapter-wallets": "^0.19.37", "@solana/web3.js": "^1.98.4", - "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.23.22", "lucide-react": "^0.544.0", "motion": "^12.23.22", "next": "15.5.4", + "next-themes": "^0.4.6", "octokit": "^5.0.3", "prisma": "^6.16.3", - "next-themes": "^0.4.6", "react": "19.1.0", "react-dom": "19.1.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1" }, "devDependencies": { diff --git a/prisma/migrations/20251004214857_update_issue_schema_for_rewards/migration.sql b/prisma/migrations/20251004214857_update_issue_schema_for_rewards/migration.sql new file mode 100644 index 0000000..7c3ba7d --- /dev/null +++ b/prisma/migrations/20251004214857_update_issue_schema_for_rewards/migration.sql @@ -0,0 +1,40 @@ +/* + Warnings: + + - The primary key for the `issues` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `tokenReward` on the `issues` table. All the data in the column will be lost. + - Added the required column `rewardAmount` to the `issues` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `issues` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterEnum +ALTER TYPE "IssueStatus" ADD VALUE 'FUNDED'; + +-- DropForeignKey +ALTER TABLE "public"."contributions" DROP CONSTRAINT "contributions_issueId_fkey"; + +-- AlterTable +ALTER TABLE "contributions" ALTER COLUMN "issueId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "issues" DROP CONSTRAINT "issues_pkey", +DROP COLUMN "tokenReward", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "githubUrl" TEXT, +ADD COLUMN "organizationWallet" TEXT, +ADD COLUMN "rewardAmount" DECIMAL(18,9) NOT NULL, +ADD COLUMN "tokenMintAddress" TEXT, +ADD COLUMN "tokenType" TEXT, +ADD COLUMN "transactionSignature" TEXT, +ADD COLUMN "treasuryWallet" TEXT, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL, +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ALTER COLUMN "githubIssueId" DROP NOT NULL, +ALTER COLUMN "number" DROP NOT NULL, +ALTER COLUMN "repoId" DROP NOT NULL, +ADD CONSTRAINT "issues_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "issues_id_seq"; + +-- AddForeignKey +ALTER TABLE "contributions" ADD CONSTRAINT "contributions_issueId_fkey" FOREIGN KEY ("issueId") REFERENCES "issues"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b5d9805..0d5a033 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,6 +19,7 @@ enum IssueStatus { OPEN CLOSED REWARDED + FUNDED } @@ -82,15 +83,23 @@ model Repository { } model Issue { - id Int @id @default(autoincrement()) - title String - githubIssueId BigInt @unique - number Int - status IssueStatus @default(OPEN) - tokenReward Decimal @db.Decimal(18, 9) - - repoId Int - repository Repository @relation(fields: [repoId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) // Changed to String to support custom IDs + title String + githubIssueId BigInt? @unique // Made optional + number Int? // Made optional + githubUrl String? // Added for GitHub issue URL + status IssueStatus @default(OPEN) + rewardAmount Decimal @db.Decimal(18, 9) // Standardized on rewardAmount + tokenType String? // Added (SOL, EOS, SPL) + tokenMintAddress String? // Added for SPL tokens + organizationWallet String? // Added for organization wallet + treasuryWallet String? // Added for treasury wallet + transactionSignature String? // Added for blockchain transaction + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + repoId Int? // Made optional + repository Repository? @relation(fields: [repoId], references: [id], onDelete: Cascade) contribution Contribution? @@index([repoId]) // for faster issue lookups by repo @@ -102,7 +111,7 @@ model Contribution { completedAt DateTime @default(now()) transactionSignature String? - issueId Int @unique + issueId String @unique // Changed to String to match Issue.id issue Issue @relation(fields: [issueId], references: [id], onDelete: Cascade) // onDelete contributorId String contributor User @relation(fields: [contributorId], references: [id], onDelete: Cascade) // onDelete