diff --git a/entropy/pharos/README.md b/entropy/pharos/README.md new file mode 100644 index 0000000..c2a3478 --- /dev/null +++ b/entropy/pharos/README.md @@ -0,0 +1,208 @@ +# Pharos Raffle + +A decentralized raffle system powered by Pyth Entropy for provably fair randomness. + +## What This Example Does + +The Pharos Raffle system enables users to create and participate in raffles with verifiable randomness. Each raffle uses Pyth Entropy to ensure fair winner selection through a commit-reveal scheme that prevents manipulation. + +### Key Features + +- **Create Raffles**: Set up raffles with custom parameters (prize type, ticket price, duration, etc.) +- **Provably Fair**: Uses Pyth Entropy for verifiable random number generation +- **Multiple Prize Types**: Support for crypto prizes (PYUSD), physical items, and digital assets +- **Factory Pattern**: Efficient contract deployment through the factory pattern +- **Frontend Interface**: Next.js application for easy interaction + +### How the Entropy Integration Works + +1. **User Purchases Tickets**: Buy PYUSD-backed tickets for a raffle +2. **Raffle Closes**: When time expires or max tickets reached, the raffle closes +3. **Randomness Request**: Contract requests randomness from Pyth Entropy with a commitment +4. **Callback Delivery**: Entropy provider calls back with random number +5. **Winner Selection**: Contract uses weighted randomness (based on ticket count) to select winner +6. **Prize Distribution**: Winner receives crypto prize, or funds go to admin for physical/digital fulfillment + +## Project Structure + +``` +entropy/Pharos/ +├── contract/ # Smart contracts built with Hardhat +│ ├── contracts/ +│ │ ├── RaffleFactory.sol # Factory contract for creating raffles +│ │ ├── Raffle.sol # Main raffle contract with Entropy integration +│ │ └── MockPYUSD.sol # Mock PYUSD token for testing +│ ├── ignition/ +│ │ └── modules/ +│ │ └── App.ts # Deployment configuration +│ ├── package.json +│ └── hardhat.config.ts +│ +└── app/ # Next.js frontend application + ├── app/ + │ └── page.tsx # Main application page + ├── components/ # UI components + ├── contracts/ # Generated contract ABIs and types + ├── providers/ # Wagmi and React Query providers + ├── package.json + └── wagmi.config.ts +``` + +## Prerequisites + +Before running this example, ensure you have: + +- **Node.js** (v18 or later) +- A Web3 wallet (e.g., MetaMask) with funds on the target network +- Access to the Pyth Entropy service on your chosen network + +## Running the Example + +### Step 1: Deploy the Smart Contracts + +Navigate to the contract directory and install dependencies: + +```bash +cd contract +npm install +``` + +Create a `.env` file with your private key and API key: + +```bash +WALLET_KEY=your_private_key_here +BLAST_SCAN_API_KEY=your_api_key_here +``` + +Deploy the contracts to Blast Sepolia testnet: + +```bash +npm run deploy +``` + +After deployment, note the deployed contract addresses, as you'll need to update them in the frontend configuration. + +### Step 2: Configure the Frontend + +Navigate to the app directory and install dependencies: + +```bash +cd ../app +npm install +``` + +Update the contract addresses in `contracts/addresses.ts` with your deployed contract addresses. + +If deploying to a different network, also update: +- `config.ts` with the correct chain configuration +- `ignition/modules/App.ts` with the appropriate Entropy contract and provider addresses for your network + +### Step 3: Run the Frontend + +Start the development server: + +```bash +npm run dev +``` + +The application will be available at http://localhost:3000. + +### Step 4: Interact with the Application + +1. **Get Test Tokens**: Use the MockPYUSD contract to get test PYUSD tokens +2. **Deposit ETH**: Deposit ETH to the factory for entropy fees +3. **Create Raffle**: Create a new raffle with your desired parameters +4. **Buy Tickets**: Purchase tickets to participate in raffles +5. **Close Raffle**: Wait for the raffle to close (time expires or max tickets reached) +6. **Winner Selection**: Random winner is selected and prize is distributed + +## Key Contract Functions + +### RaffleFactory Contract + +- **`createRaffle()`**: Creates a new raffle with specified parameters +- **`depositETH()`**: Deposit ETH to the factory for entropy fees +- **`getRaffles()`**: Get list of all created raffles + +### Raffle Contract + +- **`buyTicket(uint256 numTickets)`**: Purchase raffle tickets with PYUSD +- **`closeIfReady()`**: Automatically close raffle when ready and request randomness +- **`distributePrize()`**: Distribute prize to winner after selection + +### Events + +- **`RaffleCreated`**: Emitted when a new raffle is created +- **`TicketPurchased`**: Emitted when tickets are purchased +- **`WinnerSelected`**: Emitted when the winner is selected +- **`PrizeDistributed`**: Emitted when the prize is distributed + +## Development Notes + +### Technology Stack + +**Smart Contracts**: +- Solidity ^0.8.20 +- Hardhat for development and deployment +- OpenZeppelin contracts for security +- Pyth Entropy SDK for randomness + +**Frontend**: +- Next.js 14 with App Router +- React 18 +- Wagmi v2 for Ethereum interactions +- Viem for contract interactions +- TanStack React Query for state management +- Tailwind CSS for styling +- shadcn/ui for UI components + +### Raffle Parameters + +- **Prize Type**: Crypto, Physical, or Digital +- **Prize Amount**: Amount of PYUSD (for crypto) or description for physical/digital +- **Ticket Price**: Cost per ticket in PYUSD +- **Max Tickets**: Maximum number of tickets that can be sold +- **Max Tickets Per User**: Per-user ticket limit to prevent 51% attacks +- **Start Time**: Unix timestamp when raffle starts +- **End Time**: Unix timestamp when raffle ends +- **House Fee**: Percentage fee (in basis points, e.g., 300 = 3%) + +### Testing Locally + +To test the contracts without deploying: + +```bash +cd contract +npm test +``` + +For frontend development with a local blockchain: + +1. Start a local Hardhat node: `npx hardhat node` +2. Deploy contracts locally: `npm run deploy:local` +3. Update the frontend configuration to use the local network +4. Run the frontend: `cd ../app && npm run dev` + +Note that testing with actual Entropy requires deploying to a network where Pyth Entropy is available. + +## Supported Networks + +This example is configured for **Blast Sepolia** testnet, but can be adapted for any EVM network that supports Pyth Entropy. You'll need to: + +1. Find the Entropy contract and provider addresses for your target network in the Pyth documentation +2. Update `ignition/modules/App.ts` with the correct addresses +3. Configure the network in `hardhat.config.ts` +4. Update the frontend's `config.ts` with the chain configuration + +For available networks and addresses, see the Pyth Entropy documentation at https://docs.pyth.network/entropy. + +## Acknowledgments + +This example demonstrates how Pyth Entropy can be used to create provably fair raffle systems with verifiable randomness. + +## Additional Resources + +- **Pyth Entropy Documentation**: https://docs.pyth.network/entropy +- **Pyth Network**: https://pyth.network +- **Source Repository**: https://github.com/pyth-network/pyth-examples + diff --git a/entropy/pharos/app/.gitignore b/entropy/pharos/app/.gitignore new file mode 100644 index 0000000..d4dee41 --- /dev/null +++ b/entropy/pharos/app/.gitignore @@ -0,0 +1,71 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# hardhat +cache/ +artifacts/ +typechain-types/ +typechain/ +coverage/ +coverage.json + +# hardhat ignition +ignition/deployments/chain-*.json + +# hardhat gas reporter +gas-report.txt + +# hardhat console +.hardhat/ + +# solidity +*.sol.bak + +# deployment artifacts +deployments/ +deployments.json + +# local environment +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/entropy/pharos/app/README.md b/entropy/pharos/app/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/entropy/pharos/app/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/entropy/pharos/app/eslint.config.mjs b/entropy/pharos/app/eslint.config.mjs new file mode 100644 index 0000000..719cea2 --- /dev/null +++ b/entropy/pharos/app/eslint.config.mjs @@ -0,0 +1,25 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), + { + ignores: [ + "node_modules/**", + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ], + }, +]; + +export default eslintConfig; diff --git a/entropy/pharos/app/next.config.ts b/entropy/pharos/app/next.config.ts new file mode 100644 index 0000000..e04f9dd --- /dev/null +++ b/entropy/pharos/app/next.config.ts @@ -0,0 +1,28 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'gateway.pinata.cloud', + port: '', + pathname: '/ipfs/**', + }, + { + protocol: 'https', + hostname: 'ipfs.io', + port: '', + pathname: '/ipfs/**', + }, + { + protocol: 'https', + hostname: 'cloudflare-ipfs.com', + port: '', + pathname: '/ipfs/**', + }, + ], + }, +}; + +export default nextConfig; diff --git a/entropy/pharos/app/package.json b/entropy/pharos/app/package.json new file mode 100644 index 0000000..1c6dbdf --- /dev/null +++ b/entropy/pharos/app/package.json @@ -0,0 +1,66 @@ +{ + "name": "pharos", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build --turbopack", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@bprogress/next": "^3.2.12", + "@nomicfoundation/hardhat-toolbox": "^6.1.0", + "@openzeppelin/contracts": "^5.4.0", + "@privy-io/react-auth": "^3.3.0", + "@pythnetwork/entropy-sdk-solidity": "^2.0.0", + "@tanstack/react-query": "^5.90.5", + "dotenv": "^17.2.3", + "ethers": "^6.15.0", + "framer-motion": "^12.23.24", + "mongodb": "^6.20.0", + "next": "15.5.4", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-icons": "^5.5.0", + "wagmi": "^2.18.1" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@nomicfoundation/hardhat-chai-matchers": "^2.1.0", + "@nomicfoundation/hardhat-ethers": "^4.0.2", + "@nomicfoundation/hardhat-ignition": "^3.0.0", + "@nomicfoundation/hardhat-ignition-ethers": "^3.0.3", + "@nomicfoundation/hardhat-ignition-viem": "^3.0.0", + "@nomicfoundation/hardhat-keystore": "^3.0.0", + "@nomicfoundation/hardhat-network-helpers": "^3.0.1", + "@nomicfoundation/hardhat-node-test-runner": "^3.0.0", + "@nomicfoundation/hardhat-toolbox-viem": "^5.0.0", + "@nomicfoundation/hardhat-verify": "^3.0.3", + "@nomicfoundation/hardhat-viem": "^3.0.0", + "@nomicfoundation/hardhat-viem-assertions": "^3.0.0", + "@nomicfoundation/ignition-core": "^3.0.0", + "@tailwindcss/postcss": "^4", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^5.2.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.8.5", + "@types/react": "^19", + "@types/react-dom": "^19", + "chai": "^6.2.0", + "eslint": "^9", + "eslint-config-next": "15.5.4", + "forge-std": "https://github.com/foundry-rs/forge-std.git#v1.9.4", + "hardhat": "^3.0.7", + "hardhat-gas-reporter": "^2.3.0", + "solidity-coverage": "^0.8.16", + "tailwindcss": "^4", + "ts-node": "^10.9.2", + "typechain": "^8.3.2", + "typescript": "~5.8.0", + "viem": "^2.30.0" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", + "type": "module" +} diff --git a/entropy/pharos/app/postcss.config.mjs b/entropy/pharos/app/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/entropy/pharos/app/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/entropy/pharos/app/public/file.svg b/entropy/pharos/app/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/entropy/pharos/app/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entropy/pharos/app/public/globe.svg b/entropy/pharos/app/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/entropy/pharos/app/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entropy/pharos/app/public/next.svg b/entropy/pharos/app/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/entropy/pharos/app/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entropy/pharos/app/public/vercel.svg b/entropy/pharos/app/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/entropy/pharos/app/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entropy/pharos/app/public/window.svg b/entropy/pharos/app/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/entropy/pharos/app/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entropy/pharos/app/rules/page.tsx b/entropy/pharos/app/rules/page.tsx new file mode 100644 index 0000000..f5888ab --- /dev/null +++ b/entropy/pharos/app/rules/page.tsx @@ -0,0 +1,197 @@ +"use client"; +import React from 'react'; +import Link from 'next/link'; +import { FaWallet, FaTicketAlt, FaClock, FaGift, FaTags, FaLock, FaBalanceScale, FaShieldAlt, FaLifeRing, FaLightbulb, FaArrowLeft } from 'react-icons/fa'; + +const RulesPage = () => { + const BulletPoint = ({ children }: { children: React.ReactNode }) => ( +
+ +

{children}

+
+ ); + + return ( +
+
+ {/* Back Button */} +
+ + + Back to Home + +
+ + {/* Header */} +
+

+ 🎯 Pharos Raffle Rules & Regulations +

+

+ All participants must read and agree to these rules before participating +

+
+ + {/* Rules Sections */} +
+ {/* Rule 1 */} +
+
+

+ 🪙 1. Entry & Participation +

+
+
+ Each participant must connect a valid crypto wallet to join a raffle. + Multiple entries from different wallets are allowed, but participants are encouraged to play fairly. + Once purchased, raffle tickets are non-refundable and non-transferable. + Each raffle may have minimum and maximum ticket purchase limits, defined by the admin. +
+
+ + {/* Rule 2 */} +
+
+

+ 💰 2. Ticket Sales & Prize Pool +

+
+
+ Every raffle has a target ticket limit (e.g., 1,000 tickets). + If the total number of tickets sold is less than the limit by the end time, the available prize pool will still be distributed among winners after deducting platform fees. + All transactions are made using PYUSD or other supported stablecoins. +
+
+ + {/* Rule 3 */} +
+
+

+ 🧾 3. Raffle Closing & Winner Selection +

+
+
+ Once the raffle timer ends, the raffle will be officially closed by the Pharos Admin. + Winners will be selected through an on-chain verifiable random process (e.g., Pyth Entropy). + The results will be verified and announced by the Admin to ensure complete transparency. + Participants are requested to wait for the admin announcement of winners and distribution details. +
+
+ + {/* Rule 4 */} +
+
+

+ 🎁 4. Prize Distribution +

+
+
+ Prize distribution will be handled by the Pharos Admin after the raffle officially ends. + Winners must wait for the admin confirmation and distribution process to be completed. + Crypto prizes will be transferred directly to the winner's connected wallet. + Physical prizes (e.g., shoes, concert tickets, or merchandise) require the winner to contact the admin for verification and delivery coordination. + Physical prizes are available only in eligible countries (listed per raffle). + If the winner's country is not eligible, the equivalent prize value in PYUSD will be credited instead. +
+
+ + {/* Rule 5 */} +
+
+

+ ⏰ 5. Timeline & Claiming +

+
+
+ Each raffle has a clearly visible start and end time within the app. + Winners must claim or confirm their prize within 15 days of announcement. + Unclaimed rewards will be reallocated or added to the next prize pool. +
+
+ + {/* Rule 6 */} +
+
+

+ ⚙️ 6. Platform Fees & Transparency +

+
+
+ A small platform fee (2–5%) will be deducted from each raffle pool for operational costs. + All fees, wallet addresses, and transaction data are publicly verifiable on-chain. +
+
+ + {/* Rule 7 */} +
+
+

+ ⚖️ 7. Compliance & Responsibility +

+
+
+ Participants must ensure compliance with their local crypto and raffle laws. + Pharos is not responsible for legal restrictions in certain regions. + Any fraudulent, automated, or suspicious activity may result in disqualification and wallet blocking. +
+
+ + {/* Rule 8 */} +
+
+

+ 🔒 8. Security & Privacy +

+
+
+ Pharos does not collect personal data. + Users are responsible for the security of their wallet and private keys. + The platform will never request private keys or sensitive information. +
+
+ + {/* Rule 9 */} +
+
+

+ 📞 9. Support & Dispute Resolution +

+
+
+ For ticket or prize-related issues, contact the Pharos Admin team via the official channels. + In case of disputes, the Admin's decision will be final until DAO-based governance is implemented. +
+
+ + {/* Rule 10 */} +
+
+

+ 💡 10. Updates to Rules +

+
+
+ Pharos reserves the right to update or modify rules based on platform evolution. + All updates will be announced transparently and reflected in the app or official channels. +
+
+
+ + {/* Footer Notice */} +
+

+ ⚠️ Important Notice +

+

+ By participating in any Pharos raffle, you acknowledge that you have read, understood, and agree to abide by these rules and regulations. Failure to comply may result in disqualification or account suspension. +

+
+
+
+ ); +}; + +export default RulesPage; diff --git a/entropy/pharos/app/scripts/deploy.ts b/entropy/pharos/app/scripts/deploy.ts new file mode 100644 index 0000000..4c00d2a --- /dev/null +++ b/entropy/pharos/app/scripts/deploy.ts @@ -0,0 +1,78 @@ +import { ethers } from "ethers"; +import hre from "hardhat"; +import * as dotenv from "dotenv"; + +// Load environment variables +dotenv.config(); + +async function main() { + const ENTROPY_ADDRESS = "0x549ebba8036ab746611b4ffa1423eb0a4df61440"; // Arbitrum Entropy + const DEFAULT_PROVIDER = "0x6CC14824Ea2918f5De5C2f75A9Da968ad4BD6344"; + const PYUSD_TOKEN_ADDRESS = process.env.NEXT_PUBLIC_PYUSD_TOKEN_ADDRESS; // Mock PYUSD on Arbitrum Sepolia + const FUNDING_AMOUNT = ethers.parseEther("0.001"); // ETH to seed each new raffle + + // Validate environment variables + if (!PYUSD_TOKEN_ADDRESS) { + throw new Error("PYUSD_TOKEN_ADDRESS environment variable is not set"); + } + if (!process.env.PRIVATE_KEY) { + throw new Error("PRIVATE_KEY environment variable is not set"); + } + if (!process.env.RPC) { + throw new Error("RPC environment variable is not set"); + } + + console.log("Deployment Configuration:"); + console.log("- Entropy Address:", ENTROPY_ADDRESS); + console.log("- Default Provider:", DEFAULT_PROVIDER); + console.log("- PYUSD Token Address:", PYUSD_TOKEN_ADDRESS); + + // Get the contract artifact + const contractArtifact = await hre.artifacts.readArtifact("RaffleFactory"); + + // Create a provider and signer for hardhat network + const provider = new ethers.JsonRpcProvider(process.env.RPC); + const signer = new ethers.Wallet(process.env.PRIVATE_KEY as string, provider); + + // Create contract factory using ethers directly + const RaffleFactory = new ethers.ContractFactory( + contractArtifact.abi, + contractArtifact.bytecode, + signer + ); + + console.log("\nDeploying RaffleFactory..."); + + try { + const factory = await RaffleFactory.deploy( + ENTROPY_ADDRESS, + DEFAULT_PROVIDER, + PYUSD_TOKEN_ADDRESS, + FUNDING_AMOUNT + ); + + await factory.waitForDeployment(); + const factoryAddress = await factory.getAddress(); + + console.log("\n✅ Deployment Successful!"); + console.log("RaffleFactory deployed to:", factoryAddress); + console.log("PYUSD Token Address:", PYUSD_TOKEN_ADDRESS); + console.log("Funding amount:", ethers.formatEther(FUNDING_AMOUNT), "ETH"); + + console.log("\n📋 Next Steps:"); + console.log("1. Update your .env.local file with:"); + console.log(` NEXT_PUBLIC_FACTORY_ADDRESS=${factoryAddress}`); + console.log("2. Fund the factory with ETH for raffle creation"); + console.log("3. Test creating a raffle with startTime"); + + } catch (error) { + console.error("\n❌ Deployment failed:"); + console.error(error); + throw error; + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/entropy/pharos/app/scripts/depositETHToFactory.ts b/entropy/pharos/app/scripts/depositETHToFactory.ts new file mode 100644 index 0000000..1c4ce52 --- /dev/null +++ b/entropy/pharos/app/scripts/depositETHToFactory.ts @@ -0,0 +1,50 @@ +import { ethers } from "ethers"; +import hre from "hardhat"; +import * as dotenv from "dotenv"; + +// Load environment variables +dotenv.config(); + +async function main() { + const FACTORY_ADDRESS = process.env.NEXT_PUBLIC_FACTORY_ADDRESS as string; // Update with your factory address + const DEPOSIT_AMOUNT = ethers.parseEther("0.1"); // Deposit 0.1 ETH for entropy fees + + // Get the contract artifact + const contractArtifact = await hre.artifacts.readArtifact("RaffleFactory"); + + // Create a provider and signer + const provider = new ethers.JsonRpcProvider(process.env.RPC); + const signer = new ethers.Wallet(process.env.PRIVATE_KEY as string, provider); + + // Create contract instance + const factory = new ethers.Contract(FACTORY_ADDRESS, contractArtifact.abi, signer); + + console.log("Depositing ETH to factory..."); + console.log("Factory Address:", FACTORY_ADDRESS); + console.log("Deposit Amount:", ethers.formatEther(DEPOSIT_AMOUNT), "ETH"); + + // Check current balance + const currentBalance = await factory.getFactoryBalance(); + console.log("Current Factory Balance:", ethers.formatEther(currentBalance), "ETH"); + + // Deposit ETH + const tx = await factory.depositETH({ value: DEPOSIT_AMOUNT }); + console.log("Transaction hash:", tx.hash); + + const receipt = await tx.wait(); + console.log("Transaction confirmed:", receipt?.status === 1 ? "Success" : "Failed"); + + // Check new balance + const newBalance = await factory.getFactoryBalance(); + const entropyReserve = await factory.getEntropyFeeReserve(); + + console.log("New Factory Balance:", ethers.formatEther(newBalance), "ETH"); + console.log("Entropy Fee Reserve:", ethers.formatEther(entropyReserve), "ETH"); + + console.log("✅ ETH deposited successfully!"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/entropy/pharos/app/scripts/withdrawFromOldFactory.ts b/entropy/pharos/app/scripts/withdrawFromOldFactory.ts new file mode 100644 index 0000000..22402f9 --- /dev/null +++ b/entropy/pharos/app/scripts/withdrawFromOldFactory.ts @@ -0,0 +1,51 @@ +import { ethers } from "ethers"; +import hre from "hardhat"; +import * as dotenv from "dotenv"; + +// Load environment variables +dotenv.config(); + +async function main() { + const OLD_FACTORY_ADDRESS = process.env.OLD_FACTORY_ADDRESS as string; // Your old factory address + + // Get the contract artifact + const contractArtifact = await hre.artifacts.readArtifact("RaffleFactory"); + + // Create a provider and signer + const provider = new ethers.JsonRpcProvider(process.env.RPC); + const signer = new ethers.Wallet(process.env.PRIVATE_KEY as string, provider); + + // Create contract instance + const factory = new ethers.Contract(OLD_FACTORY_ADDRESS, contractArtifact.abi, signer); + + console.log("Withdrawing ETH from old factory..."); + console.log("Old Factory Address:", OLD_FACTORY_ADDRESS); + + // Check current balance + const currentBalance = await factory.getFactoryBalance(); + console.log("Current Factory Balance:", ethers.formatEther(currentBalance), "ETH"); + + if (currentBalance === BigInt(0)) { + console.log("❌ No ETH to withdraw from old factory"); + return; + } + + // Withdraw all ETH + const tx = await factory.withdrawETH(); + console.log("Transaction hash:", tx.hash); + + const receipt = await tx.wait(); + console.log("Transaction confirmed:", receipt?.status === 1 ? "Success" : "Failed"); + + // Check new balance + const newBalance = await factory.getFactoryBalance(); + console.log("New Factory Balance:", ethers.formatEther(newBalance), "ETH"); + + console.log("✅ ETH withdrawn successfully!"); + console.log("💡 Now deploy the new factory and deposit ETH to it"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/entropy/pharos/app/src/Components/AdminDashboard/AdminDashboard.tsx b/entropy/pharos/app/src/Components/AdminDashboard/AdminDashboard.tsx new file mode 100644 index 0000000..827b2dc --- /dev/null +++ b/entropy/pharos/app/src/Components/AdminDashboard/AdminDashboard.tsx @@ -0,0 +1,1254 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { usePrivy } from "@privy-io/react-auth"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; +import { MdAdminPanelSettings, MdImage, MdUpload } from "react-icons/md"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; +import { ethers, Log } from "ethers"; +import { uploadImage } from "@/lib/imageUpload"; +import AdminDashboardSkeleton from "../SkeletonLoader/AdminDashboardSkeleton"; + +// Contract addresses +const FACTORY_ADDRESS = + process.env.NEXT_PUBLIC_FACTORY_ADDRESS || + "0x0000000000000000000000000000000000000000"; +const PYUSD_TOKEN_ADDRESS = + process.env.NEXT_PUBLIC_PYUSD_TOKEN_ADDRESS || + "0x79Bd6F9E7B7B25B343C762AE5a35b20353b2CCb8"; + +// Admin wallet addresses from environment variables +const ADMIN_ADDRESSES = process.env.NEXT_PUBLIC_ADMIN_ADDRESSES + ? process.env.NEXT_PUBLIC_ADMIN_ADDRESSES.split(",").map((addr) => + addr.trim() + ) + : []; + +interface RaffleFormData { + image: File | null; + imagePreview: string; + imageUrl: string; // Pinata IPFS URL + title: string; + description: string; + pricePerTicket: string; + startDate: string; + startTime: string; + endDate: string; + endTime: string; + availableTickets: string; + maxTicketsPerUser: string; + totalPrizePool: string; + category: string; + houseFeePercentage: string; +} + +const AdminDashboard = () => { + const { authenticated, user, login } = usePrivy(); + const router = useRouter(); + const [isAdmin, setIsAdmin] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const [imageError, setImageError] = useState(""); + const [raffleAddress, setRaffleAddress] = useState(""); + const [txHash, setTxHash] = useState(""); + const [createRaffleError, setCreateRaffleError] = useState( + null + ); + + const [formData, setFormData] = useState({ + image: null, + imagePreview: "", + imageUrl: "", + title: "", + description: "", + pricePerTicket: "", + startDate: "", + startTime: "", + endDate: "", + endTime: "", + availableTickets: "", + maxTicketsPerUser: "5", + totalPrizePool: "", + category: "General", + houseFeePercentage: "3", + }); + + useEffect(() => { + // Check if user is authenticated and is an admin + if (!authenticated) { + setIsLoading(false); + return; + } + + const userAddress = user?.wallet?.address?.toLowerCase(); + const isUserAdmin = ADMIN_ADDRESSES.some( + (addr) => addr.toLowerCase() === userAddress + ); + + setIsAdmin(isUserAdmin); + setIsLoading(false); + }, [authenticated, user]); + + const handleImageChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Reset error + setImageError(""); + + // Validate image is square + const img = new window.Image(); + const objectUrl = URL.createObjectURL(file); + + img.onload = async () => { + if (img.width === img.height) { + // Image is square, proceed with upload to Pinata + try { + setFormData((prev) => ({ + ...prev, + image: file, + imagePreview: objectUrl, + })); + + // Upload to Pinata + const uploadResult = await uploadImage(file); + + if (uploadResult.success && uploadResult.url) { + setFormData((prev) => ({ + ...prev, + imageUrl: uploadResult.url!, + })); + console.log("✅ Image uploaded to Pinata:", uploadResult.url); + } else { + setImageError(uploadResult.error || "Failed to upload image"); + } + } catch (error) { + console.error("Error uploading image:", error); + setImageError("Failed to upload image to Pinata"); + } + } else { + // Image is not square + setImageError( + `Image must be square (1:1 aspect ratio). Current dimensions: ${img.width}x${img.height}` + ); + URL.revokeObjectURL(objectUrl); + // Reset file input + e.target.value = ""; + } + }; + + img.onerror = () => { + setImageError("Failed to load image. Please try another file."); + URL.revokeObjectURL(objectUrl); + e.target.value = ""; + }; + + img.src = objectUrl; + } + }; + + const handleInputChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + > + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + // Smart contract interaction functions + const getProvider = () => { + if (typeof window !== "undefined" && window.ethereum) { + return new ethers.BrowserProvider(window.ethereum); + } + throw new Error("MetaMask not found"); + }; + + const getReadOnlyProvider = () => { + // Use direct RPC for read-only operations to avoid network issues + return new ethers.JsonRpcProvider("https://sepolia-rollup.arbitrum.io/rpc"); + }; + + const createRaffleContract = async () => { + try { + const provider = getProvider(); + const signer = await provider.getSigner(); + + // RaffleFactory ABI (compatible with old factory) + const factoryAbi = [ + "function createRaffle(uint8 _prizeType, uint256 _prizeAmount, string _prizeDescription, uint256 _ticketPrice, uint256 _maxTickets, uint256 _maxTicketsPerUser, uint256 _startTime, uint256 _endTime, uint256 _houseFeePercentage) external returns (address)", + "function fundingAmount() external view returns (uint256)", + ]; + + const factory = new ethers.Contract(FACTORY_ADDRESS, factoryAbi, signer); + + console.log( + "Using factory contract (compatible with current deployment)" + ); + + // Check factory ETH balance (only if function exists) + try { + const factoryBalance = await factory.getFactoryBalance(); + console.log( + "Factory ETH Balance:", + ethers.formatEther(factoryBalance), + "ETH" + ); + } catch (e) { + console.log("getFactoryBalance not available in current factory"); + } + + try { + const entropyReserve = await factory.getEntropyFeeReserve(); + console.log( + "Entropy Fee Reserve:", + ethers.formatEther(entropyReserve), + "ETH" + ); + } catch (e) { + console.log("getEntropyFeeReserve not available in current factory", e); + } + + // Convert form data to contract parameters + const prizeType = 0; // 0=Crypto, 1=Physical, 2=Digital + const prizeAmount = ethers.parseEther(formData.totalPrizePool); + const prizeDescription = formData.title; + const ticketPrice = ethers.parseEther(formData.pricePerTicket); + const maxTickets = parseInt(formData.availableTickets); + const maxTicketsPerUser = parseInt(formData.maxTicketsPerUser); + + // Convert date and time to Unix timestamp + const startDateTime = new Date( + `${formData.startDate}T${formData.startTime}` + ); + const startTime = Math.floor(startDateTime.getTime() / 1000); + const endDateTime = new Date(`${formData.endDate}T${formData.endTime}`); + const endTime = Math.floor(endDateTime.getTime() / 1000); + + // Validate timing + if (startTime >= endTime) { + throw new Error("Start time must be before end time"); + } + if (startTime <= Math.floor(Date.now() / 1000)) { + throw new Error("Start time must be in the future"); + } + + const houseFeePercentage = parseInt(formData.houseFeePercentage) * 100; // Convert to basis points (3% = 300) + + console.log("Creating raffle with parameters:", { + prizeType, + prizeAmount: ethers.formatEther(prizeAmount), + prizeDescription, + ticketPrice: ethers.formatEther(ticketPrice), + maxTickets, + maxTicketsPerUser, + startTime: new Date(startTime * 1000).toLocaleString(), + endTime: new Date(endTime * 1000).toLocaleString(), + houseFeePercentage, + }); + + // Try to call without ETH first, if it fails, try with ETH + let tx; + try { + tx = await factory.createRaffle( + prizeType, + prizeAmount, + prizeDescription, + ticketPrice, + maxTickets, + maxTicketsPerUser, + startTime, + endTime, + houseFeePercentage + ); + } catch (error) { + console.log("No ETH payment failed, trying with ETH payment..."); + const fundingAmount = await factory.fundingAmount(); + tx = await factory.createRaffle( + prizeType, + prizeAmount, + prizeDescription, + ticketPrice, + maxTickets, + maxTicketsPerUser, + startTime, + endTime, + houseFeePercentage, + { value: fundingAmount } + ); + } + + console.log("Transaction hash:", tx.hash); + + // Wait for transaction confirmation + const receipt = await tx.wait(); + console.log("Transaction confirmed:", receipt); + + // Extract raffle address from events using Interface decoding + let raffleAddress = null; + if (receipt?.logs && receipt.logs.length > 0) { + const eventTopic = ethers.id("RaffleCreated(address,string,uint256)"); + const raffleCreatedLog = receipt.logs.find( + (log: Log) => log.topics[0] === eventTopic + ); + + if (raffleCreatedLog) { + const iface = new ethers.Interface([ + "event RaffleCreated(address raffleAddress, string prizeDescription, uint256 fundingAmount)", + ]); + try { + const decoded = iface.parseLog({ + topics: raffleCreatedLog.topics, + data: raffleCreatedLog.data, + }); + raffleAddress = decoded?.args?.[0]; + if (raffleAddress) { + raffleAddress = ethers.getAddress(raffleAddress); + console.log("Found raffle address from event:", raffleAddress); + } + } catch (e) { + console.log( + "Failed to parse RaffleCreated event, will fallback to getRaffles()", + e + ); + } + } + } + + // Fallback: get the latest raffle from factory + if (!raffleAddress) { + try { + const factoryAbiWithGetRaffles = [ + "function createRaffle(uint8 _prizeType, uint256 _prizeAmount, string memory _prizeDescription, uint256 _ticketPrice, uint256 _maxTickets, uint256 _maxTicketsPerUser, uint256 _endTime, uint256 _houseFeePercentage) external returns (address)", + "function getRaffles() external view returns (address[] memory)", + ]; + const factoryWithGetRaffles = new ethers.Contract( + FACTORY_ADDRESS, + factoryAbiWithGetRaffles, + signer + ); + const raffles = await factoryWithGetRaffles.getRaffles(); + raffleAddress = raffles[raffles.length - 1]; + console.log("Got raffle address from getRaffles():", raffleAddress); + } catch (error) { + console.log("Could not get raffle address from getRaffles():", error); + } + } + + return { success: true, raffleAddress, txHash: tx.hash }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + console.error("Error creating raffle:", error); + return { success: false, error: errorMessage }; + } + }; + + // Function to check and fund raffle with ETH if needed + const checkAndFundRaffle = async (raffleAddress: string) => { + try { + const provider = getProvider(); + const signer = await provider.getSigner(); + + // Check current ETH balance of raffle + const raffleBalance = await provider.getBalance(raffleAddress); + console.log( + "Current raffle ETH balance:", + ethers.formatEther(raffleBalance), + "ETH" + ); + + // Get required entropy fee from the entropy contract directly + const entropyAbi = [ + "function getFee(address provider) external view returns (uint128)", + ]; + + // Arbitrum Sepolia entropy contract address + const ENTROPY_ADDRESS = "0x6CC14824Ea2918f5De5C2f75A9Da968ad4BD6344"; + const PROVIDER_ADDRESS = "0x177615c07d0c89f553cAB585C4b5dAf4bA7B2676"; + + let requiredFee = ethers.parseEther("0.01"); // Default fallback + + try { + const entropy = new ethers.Contract( + ENTROPY_ADDRESS, + entropyAbi, + provider + ); + const checksummedProvider = ethers.getAddress(PROVIDER_ADDRESS); + requiredFee = await entropy.getFee(checksummedProvider); + console.log( + "Required entropy fee:", + ethers.formatEther(requiredFee), + "ETH" + ); + } catch (error) { + console.log( + "Could not get entropy fee, using default:", + ethers.formatEther(requiredFee), + "ETH", + error + ); + } + + // Check if funding is needed (add small buffer for safety) + const bufferAmount = ethers.parseEther("0.001"); // 0.001 ETH buffer + const totalRequired = requiredFee + bufferAmount; + + if (raffleBalance < totalRequired) { + const fundingAmount = totalRequired - raffleBalance; + console.log( + "Funding needed:", + ethers.formatEther(fundingAmount), + "ETH" + ); + console.log("Required fee:", ethers.formatEther(requiredFee), "ETH"); + console.log("Buffer amount:", ethers.formatEther(bufferAmount), "ETH"); + + // Check user's ETH balance + const userBalance = await provider.getBalance(signer.address); + console.log( + "Your ETH balance:", + ethers.formatEther(userBalance), + "ETH" + ); + + if (userBalance < fundingAmount) { + throw new Error( + `Insufficient ETH balance. Need ${ethers.formatEther( + fundingAmount + )} ETH, have ${ethers.formatEther(userBalance)} ETH` + ); + } + + // Send ETH to raffle with higher gas settings + console.log("Sending ETH to raffle..."); + console.log( + "Funding amount:", + ethers.formatEther(fundingAmount), + "ETH" + ); + + const tx = await signer.sendTransaction({ + to: raffleAddress, + value: fundingAmount, + gasLimit: 100000, // Higher gas limit + gasPrice: ethers.parseUnits("2", "gwei"), // Higher gas price for Arbitrum Sepolia + }); + + console.log("Funding transaction hash:", tx.hash); + console.log("Waiting for confirmation..."); + + const receipt = await tx.wait(); + console.log( + "Funding transaction confirmed:", + receipt?.status === 1 ? "Success" : "Failed" + ); + + // Verify new balance + const newBalance = await provider.getBalance(raffleAddress); + console.log( + "New raffle ETH balance:", + ethers.formatEther(newBalance), + "ETH" + ); + + // Double-check that we have enough ETH now + if (newBalance < totalRequired) { + throw new Error( + `Funding failed. Still insufficient ETH. Have ${ethers.formatEther( + newBalance + )}, need ${ethers.formatEther(totalRequired)}` + ); + } + + return { funded: true, amount: ethers.formatEther(fundingAmount) }; + } else { + console.log("Raffle already has sufficient ETH"); + return { funded: false, amount: "0" }; + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + console.error("Error funding raffle:", error); + throw new Error(`Failed to fund raffle: ${errorMessage}`); + } + }; + + // Function to close raffle with automatic funding + // const closeRaffleWithFunding = async (raffleAddress: string) => { + // try { + // console.log('🔒 Starting raffle closure process...') + + // // Step 1: Check and fund if needed + // console.log('💰 Checking ETH balance and funding if needed...') + // const fundingResult = await checkAndFundRaffle(raffleAddress) + + // if (fundingResult.funded) { + // console.log(`✅ Funded raffle with ${fundingResult.amount} ETH`) + // } + + // // Step 2: Close the raffle using auto-close function + // console.log('🔒 Auto-closing raffle...') + // const provider = getProvider() + // const signer = await provider.getSigner() + + // // Use direct RPC for reading contract data + // const readProvider = getReadOnlyProvider() + + // const raffleAbi = [ + // "function closeIfReady() external", + // "function owner() external view returns (address)" + // ] + + // const raffleRead = new ethers.Contract(raffleAddress, raffleAbi, readProvider) + // const raffleWrite = new ethers.Contract(raffleAddress, raffleAbi, signer) + + // // Check if user is the owner (required for admin-only close) using read provider + // const owner = await raffleRead.owner() + // const userAddress = await signer.getAddress() + + // if (owner.toLowerCase() !== userAddress.toLowerCase()) { + // throw new Error('Only the raffle owner can close this raffle') + // } + + // console.log('Auto-closing raffle...') + + // const tx = await raffleWrite.closeIfReady() + // console.log('Close transaction hash:', tx.hash) + + // const receipt = await tx.wait() + + // console.log('Close transaction receipt:', { + // status: receipt?.status, + // logsCount: receipt?.logs?.length || 0, + // gasUsed: receipt?.gasUsed?.toString() + // }) + + // // Try to find the RaffleClosed event + // let sequenceNumber = null + // if (receipt?.logs && receipt.logs.length > 0) { + // const raffleClosedTopic = ethers.id("RaffleClosed(uint64)") + // const raffleClosedLog = receipt.logs.find((log: Log) => log.topics[0] === raffleClosedTopic) + + // if (raffleClosedLog) { + // // Decode the event data using the correct ABI + // const eventAbi = ["event RaffleClosed(uint64 sequenceNumber)"] + // const iface = new ethers.Interface(eventAbi) + // try { + // const decoded = iface.parseLog({ + // topics: raffleClosedLog.topics, + // data: raffleClosedLog.data + // }) + // sequenceNumber = decoded?.args?.[0] + // } catch (e) { + // console.log('Failed to parse RaffleClosed event:', e) + // // Fallback: extract from topics directly + // if (raffleClosedLog.topics.length > 1) { + // sequenceNumber = BigInt(raffleClosedLog.topics[1]) + // } + // } + // } + // } + + // if (sequenceNumber) { + // console.log('✅ Raffle closed successfully!') + // console.log('📊 Sequence number:', sequenceNumber) + + // return { + // success: true, + // sequenceNumber: sequenceNumber.toString(), + // funded: fundingResult.funded, + // fundingAmount: fundingResult.amount + // } + // } else { + // throw new Error('Could not find sequence number in transaction logs') + // } + + // } catch (error) { + // const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + // console.error('Error closing raffle:', error) + // throw new Error(`Failed to close raffle: ${errorMessage}`) + // } + // } + + // Function to save raffle data to MongoDB + const saveRaffleToMongoDB = async ( + contractAddress: string, + txHash: string + ) => { + try { + const raffleData = { + contractAddress, + title: formData.title, + description: formData.description, + imageUrl: formData.imageUrl, + pricePerTicket: formData.pricePerTicket, + totalTickets: parseInt(formData.availableTickets), + ticketsSold: 0, + startTime: Math.floor( + new Date(`${formData.startDate}T${formData.startTime}`).getTime() / + 1000 + ), + endTime: Math.floor( + new Date(`${formData.endDate}T${formData.endTime}`).getTime() / 1000 + ), + isClosed: false, + maxTicketsPerUser: parseInt(formData.maxTicketsPerUser), + houseFeePercentage: parseInt(formData.houseFeePercentage), + prizeAmount: formData.totalPrizePool, + category: formData.category, + txHash, + createdBy: user?.wallet?.address || "", + }; + + const response = await fetch("/api/raffles", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(raffleData), + }); + + if (!response.ok) { + throw new Error("Failed to save raffle to database"); + } + + const result = await response.json(); + console.log("✅ Raffle saved to MongoDB:", result); + return result; + } catch (error) { + console.error("Error saving raffle to MongoDB:", error); + throw error; + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setCreateRaffleError(null); // Clear previous errors + + try { + // Create raffle on blockchain + const result = await createRaffleContract(); + + if (result.success && result.raffleAddress && result.txHash) { + console.log("✅ Raffle created successfully!"); + console.log("Raffle Address:", result.raffleAddress); + console.log("Transaction Hash:", result.txHash); + + // Save to MongoDB + try { + await saveRaffleToMongoDB(result.raffleAddress, result.txHash); + console.log("✅ Raffle data saved to MongoDB"); + } catch (dbError) { + console.error( + "⚠️ Failed to save to MongoDB, but blockchain transaction succeeded:", + dbError + ); + // Don't fail the entire process if MongoDB fails + } + + setRaffleAddress(result.raffleAddress); + setTxHash(result.txHash); + setShowSuccess(true); + + // Reset form after 5 seconds + setTimeout(() => { + setFormData({ + image: null, + imagePreview: "", + imageUrl: "", + title: "", + description: "", + pricePerTicket: "", + startDate: "", + startTime: "", + endDate: "", + endTime: "", + availableTickets: "", + maxTicketsPerUser: "5", + totalPrizePool: "", + category: "General", + houseFeePercentage: "3", + }); + setShowSuccess(false); + setRaffleAddress(""); + setTxHash(""); + }, 5000); + } else { + setCreateRaffleError(result.error || "Unknown error occurred"); + // Clear error after 10 seconds + setTimeout(() => { + setCreateRaffleError(null); + }, 10000); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "An unexpected error occurred"; + console.error("Error:", error); + setCreateRaffleError(errorMessage); + // Clear error after 10 seconds + setTimeout(() => { + setCreateRaffleError(null); + }, 10000); + } finally { + setIsSubmitting(false); + } + }; + + // Loading state + if (isLoading) { + return ; + } + + // Not authenticated + if (!authenticated) { + return ( +
+
+
+ +

+ Admin Access Required +

+

+ You need to connect your wallet to access the admin dashboard +

+ +
+
+
+ ); + } + + // Not admin + if (!isAdmin) { + return ( +
+
+
+
+ + + +
+

+ Access Denied +

+

+ You don't have permission to access this page +

+ +
+
+
+ ); + } + + // Admin dashboard + return ( +
+
+ {/* Header */} +
+
+ +
+

+ Admin Dashboard +

+

+ Create and manage PYUSD raffles +

+
+
+ + {/* Contract Info */} +
+

+ 📋 Contract Information: +

+
+
+ Factory: + {FACTORY_ADDRESS} +
+
+ PYUSD Token: + {PYUSD_TOKEN_ADDRESS} +
+
+
+
+ + {/* Success Message */} + {showSuccess && ( +
+
+
🎉
+
+

+ Raffle Created Successfully! +

+

+ Your raffle has been deployed to the blockchain. +

+
+
+ + {raffleAddress && ( +
+

+ Contract + Raffle Address +

+
+

+ {raffleAddress} +

+
+
+ )} + + {txHash && ( +
+

+ TX + Transaction Hash +

+
+

+ {txHash} +

+
+
+ )} +
+ )} + + {/* Error Message */} + {createRaffleError && ( +
+
+
+
+

+ Error Creating Raffle +

+

+ Something went wrong during raffle creation. +

+
+
+ +
+

+ Error Details: +

+
+

+ {createRaffleError} +

+
+
+ +
+
+
💡
+
+

+ Note +

+

+ Factory automatically funds raffles from its ETH reserve. No manual ETH payment needed. +

+
+
+
+
+ )} + + {/* Raffle Creation Form */} +
+

+ + Create New Raffle + +

+ +
+ {/* Image Upload */} +
+ +
+ {formData.imagePreview ? ( +
+
+ Preview +
+

+ ✓ Square image uploaded successfully +

+ {formData.imageUrl && ( +

+ 📡 Stored on IPFS: {formData.imageUrl.slice(0, 50)} + ... +

+ )} +
+
+ +
+ ) : ( + + )} +
+ + {/* Error Message */} + {imageError && ( +
+

+ + {imageError} +

+

+ Please upload a square image with equal width and height + (e.g., 500x500px, 800x800px, 1000x1000px) +

+
+ )} + + {/* Info Message */} + {!formData.imagePreview && !imageError && ( +
+

+ 💡 Tip: Image upload is optional. Use square images (1:1 + ratio) for best display. Examples: 500x500px, 800x800px, or + 1000x1000px +

+
+ )} +
+ + {/* Raffle Title */} +
+ + +
+ + {/* Description */} +
+ +