diff --git a/PROJECT_BLUEPRINT.md b/PROJECT_BLUEPRINT.md new file mode 100644 index 0000000..ba2a29d --- /dev/null +++ b/PROJECT_BLUEPRINT.md @@ -0,0 +1,704 @@ +# พิมพ์เขียวโครงการ ZeaZDev Mini App (Project Blueprint) + +**Version:** 2.0 - World App Mini App Edition +**Developer:** PHIPHAT PHOEMSUK (ZeaZDev) +**Website:** https://app.zeaz.dev/ +**Platform:** World App Mini App +**License:** MIT + +--- + +## 1. บทสรุปโครงการ (Project Overview) + +### Mission Statement +ZeaZDev คือ World App Mini App ที่ใช้เทคโนโลยี Zero-Knowledge Proof (ZKP) ผ่าน World ID สำหรับการยืนยันตัวตน (Proof of Personhood) และให้บริการทางการเงินแบบ DeFi บน Blockchain โดยผู้ใช้จะได้รับรางวัลรายวัน, Airdrop และสามารถทำธุรกรรมแบบ Swap Token ได้อย่างปลอดภัยและไม่ต้องจ่าย Gas (Gasless Transactions) + +### Problem Statement +ปัญหาหลักที่ ZeaZDev Mini App แก้ไข: +- **Sybil Attack ในระบบรางวัล:** ผู้ใช้สามารถสร้างหลาย Account เพื่อรับรางวัลซ้ำ ทำให้การแจกจ่ายไม่เป็นธรรม +- **ค่า Gas ที่สูง:** ผู้ใช้ต้องจ่ายค่า Gas เองในการทำธุรกรรม ทำให้ผู้เริ่มต้นไม่สามารถเข้าถึงได้ +- **การยืนยันตัวตนที่ซับซ้อน:** ระบบ KYC แบบเดิมต้องเปิดเผยข้อมูลส่วนตัว ขาดความเป็นส่วนตัว +- **UX ที่ซับซ้อนในการใช้ DeFi:** ผู้ใช้ทั่วไปไม่เข้าใจวิธีใช้ Wallet, Swap และ DeFi Tools +- **ขาดระบบ Incentive สำหรับผู้ใช้:** ไม่มีแรงจูงใจให้ผู้ใช้กลับมาใช้งานอย่างต่อเนื่อง + +### Solution +ZeaZDev Mini App แก้ไขปัญหาเหล่านี้ด้วย: +- **World ID Integration (ZKP):** ใช้ Zero-Knowledge Proof เพื่อยืนยันว่าผู้ใช้เป็นมนุษย์จริง (Proof of Personhood) โดยไม่ต้องเปิดเผยข้อมูลส่วนตัว และป้องกัน Sybil Attack ด้วย Nullifier Hash +- **Gasless Transactions (Meta-Transactions):** ระบบ Relayer ช่วยจ่ายค่า Gas ให้ผู้ใช้ ทำให้ผู้เริ่มต้นสามารถใช้งานได้ทันที +- **Daily Check-in Rewards with Streak:** ระบบรางวัลรายวันที่สร้างแรงจูงใจด้วย Streak System เพื่อให้ผู้ใช้กลับมาใช้งานต่อเนื่อง +- **One-time Airdrop:** รางวัลต้อนรับสำหรับผู้ใช้ที่ผ่านการยืนยัน World ID +- **DEX Integration:** ระบบ Swap Token ที่เชื่อมต่อกับ Decentralized Exchange (เช่น Uniswap) ภายใน Mini App +- **React Native (Expo) UI/UX:** Interface ที่เรียบง่าย เหมาะสำหรับ Mobile-first และทำงานบน World App + +--- + +## 1.1 World ID Zero-Knowledge Proof Workflow (ขั้นตอนการยืนยันตัวตน) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ World ID ZKP Workflow │ +└─────────────────────────────────────────────────────────────────┘ + +Step 1: Frontend (Mini App) +┌──────────────────────────────────────┐ +│ User opens ZeaZDev Mini App │ +│ → Clicks "Verify with World ID" │ +│ → IDKit widget opens │ +└──────────┬───────────────────────────┘ + │ + ▼ +Step 2: World App (Verification) +┌──────────────────────────────────────┐ +│ User scans with World ID Orb/Device │ +│ → Generates Zero-Knowledge Proof │ +│ → Returns: │ +│ • merkle_root │ +│ • nullifier_hash (unique ID) │ +│ • proof (ZKP array) │ +└──────────┬───────────────────────────┘ + │ + ▼ +Step 3: Backend Verifier (Node.js) +┌──────────────────────────────────────┐ +│ Receives proof from frontend │ +│ → Calls World ID API for validation │ +│ → Checks nullifier_hash uniqueness │ +│ → If valid, proceeds to Step 4 │ +└──────────┬───────────────────────────┘ + │ + ▼ +Step 4: Smart Contract (On-chain) +┌──────────────────────────────────────┐ +│ Backend calls verifyAndRegister() │ +│ → Contract verifies ZKP on-chain │ +│ → Stores nullifier_hash │ +│ → Marks user as verified │ +│ → Emits UserVerified event │ +└──────────┬───────────────────────────┘ + │ + ▼ +Step 5: Access Granted +┌──────────────────────────────────────┐ +│ User can now: │ +│ ✓ Claim Airdrop (one-time) │ +│ ✓ Daily Check-in for rewards │ +│ ✓ Use Wallet features │ +│ ✓ Swap tokens │ +└──────────────────────────────────────┘ + +Key Security Features: +• Nullifier Hash prevents same person from verifying twice +• Zero-Knowledge Proof ensures privacy (no personal data stored) +• On-chain verification ensures tamper-proof records +• Relayer system provides gasless transactions +``` + +--- + +## 2. ผู้ใช้และบทบาท (User Roles & Personas) + +### 2.1 System Administrator (เจ้าของระบบ) +**คำอธิบาย:** ผู้ดูแลระบบหลักที่มีสิทธิ์ในการ Deploy และจัดการ Smart Contract + +**สิทธิ์และหน้าที่:** +- Deploy Smart Contract บนหลาย Network +- จัดการการกระจาย Token +- ตั้งค่าระบบรางวัลและ Airdrop +- ดูแลระบบ Backend และ API +- ตรวจสอบและจัดการ Logs + +**ความต้องการ:** +- เข้าใจ Blockchain และ Smart Contract เบื้องต้น +- มี Private Key สำหรับ Deployment +- มี RPC Endpoints ของ Network ที่ต้องการ Deploy + +### 2.2 Relayer (ผู้ดำเนินการทรานแซคชัน) +**คำอธิบาย:** Wallet อัตโนมัติที่จัดการจ่าย Gas และดำเนินการทรานแซคชันแทนผู้ใช้ + +**สิทธิ์และหน้าที่:** +- ดำเนินการทรานแซคชันแทนผู้ใช้ (Gasless Transaction) +- จัดการระบบรางวัลอัตโนมัติ +- ตรวจสอบ WorldID Verification + +**ความต้องการ:** +- Hot Wallet พร้อม Gas สำหรับทำธุรกรรม +- การรักษาความปลอดภัย Private Key + +### 2.3 End User (ผู้ใช้ปลายทาง) +**คำอธิบาย:** ผู้ใช้ทั่วไปที่ต้องการรับ Airdrop หรือรางวัล + +**สิทธิ์และหน้าที่:** +- เชื่อมต่อ Wallet (MetaMask, WalletConnect) +- รับ Airdrop ผ่านระบบ Merkle Proof +- รับรางวัลจากกิจกรรมต่างๆ +- ยืนยันตัวตนผ่าน WorldID (ถ้าต้องการ) + +**ความต้องการ:** +- Wallet ที่รองรับ (MetaMask, WalletConnect) +- อยู่ใน Whitelist สำหรับ Airdrop +- WorldID สำหรับฟีเจอร์บางอย่าง + +### 2.4 Developer (นักพัฒนา) +**คำอธิบาย:** นักพัฒนาที่ต้องการนำ ZeaZDev ไปใช้ในโปรเจกต์ของตนเอง + +**สิทธิ์และหน้าที่:** +- Customize Smart Contract +- ปรับแต่ง Frontend และ Dashboard +- เพิ่มฟีเจอร์ใหม่ๆ +- Integration กับระบบอื่น + +**ความต้องการ:** +- ความรู้ Solidity, JavaScript/TypeScript +- ความเข้าใจ Hardhat, ethers.js +- ประสบการณ์ Web3 Development + +--- + +## 3. ฟีเจอร์หลักและฟังก์ชัน (Core Features & Functionality) + +### 3.1 Smart Contract Deployment System +**คำอธิบาย:** ระบบ Deploy Smart Contract อัตโนมัติบนหลาย Network + +**รายละเอียด:** +- รองรับการ Deploy พร้อมกันบนหลาย Network +- ตั้งค่า RPC endpoints ผ่าน Environment Variables +- Verify Contract บน Block Explorer อัตโนมัติ +- จัดการ Gas Price และ Nonce อัตโนมัติ + +**User Story:** +> "ในฐานะ System Administrator ฉันต้องการ Deploy Smart Contract บน WorldChain, Base และ Sepolia พร้อมกัน เพื่อประหยัดเวลาและลดข้อผิดพลาด โดยระบบควรจัดการการตั้งค่าและ Verification อัตโนมัติ" + +### 3.2 Merkle-based Airdrop System +**คำอธิบาย:** ระบบแจกจ่าย Token แบบ Airdrop โดยใช้ Merkle Tree + +**รายละเอียด:** +- สร้าง Merkle Tree จาก Whitelist อัตโนมัติ +- Merkle Proof Verification On-chain +- ประหยัด Gas อย่างมาก +- ป้องกันการ Claim ซ้ำ +- รองรับจำนวนผู้รับหลายพันคนได้ + +**User Story:** +> "ในฐานะ End User ฉันต้องการตรวจสอบว่าฉันมีสิทธิ์รับ Airdrop หรือไม่ และสามารถ Claim Token ได้ทันทีผ่านหน้าเว็บ โดยไม่ต้องจ่าย Gas เอง" + +### 3.3 Reward Distribution System +**คำอธิบาย:** ระบบแจกจ่ายรางวัลแบบ On-chain พร้อมระบบป้องกันการจ่ายซ้ำ + +**รายละเอียด:** +- แจกรางวัลผ่าน Smart Contract +- Idempotency Key เพื่อป้องกันการจ่ายซ้ำ +- Event Logging สำหรับตรวจสอบ +- Admin Withdraw สำหรับดึง Token ที่เหลือ + +**User Story:** +> "ในฐานะ System Administrator ฉันต้องการแจกรางวัลให้ผู้ใช้ที่ทำกิจกรรม Check-in ทุกวัน โดยระบบควรป้องกันการจ่ายซ้ำและตรวจสอบได้ทุกธุรกรรม" + +### 3.4 WorldID Integration +**คำอธิบาย:** การผสานระบบยืนยันตัวตนด้วย WorldID + +**รายละเอียด:** +- ยืนยันตัวตนแบบ Proof of Personhood +- ป้องกัน Sybil Attack +- รองรับ IDKit สำหรับ Frontend +- Backend Verification API + +**User Story:** +> "ในฐานะ End User ฉันต้องการยืนยันตัวตนผ่าน WorldID เพื่อรับรางวัลพิเศษ โดยไม่ต้องเปิดเผยข้อมูลส่วนตัว" + +### 3.5 Multi-chain Support +**คำอธิบาย:** รองรับการทำงานบนหลาย Blockchain + +**รายละเอียด:** +- WorldChain (Primary) +- Base +- Sepolia (Testnet) +- Ethereum Mainnet +- สามารถเพิ่ม Network ใหม่ได้ง่าย + +**User Story:** +> "ในฐานะ Developer ฉันต้องการ Deploy ระบบเดียวกันบนหลาย Network เพื่อให้ผู้ใช้เลือกใช้ Network ที่ Gas ถูกที่สุด" + +--- + +## 4. Technology Stack ที่แนะนำ (Recommended Tech Stack) + +### Frontend (Mini App UI) +- **Framework:** React Native ^0.73 with Expo ~50.0 +- **Navigation:** Expo Router ~3.4 +- **World ID SDK:** @worldcoin/idkit-core ^1.0 +- **Web3 Library:** ethers ^6.10 +- **State Management:** @react-native-async-storage/async-storage +- **UI Components:** Custom React Native components with StyleSheet +- **Safe Area:** react-native-safe-area-context +- **Language:** TypeScript ^5.3 + +**Key Files:** +- `mini-app/src/screens/AuthGate.tsx` - World ID verification gate +- `mini-app/src/screens/WalletScreen.tsx` - Wallet management +- `mini-app/src/screens/RewardScreen.tsx` - Daily check-in & airdrop +- `mini-app/src/screens/SwapTradeScreen.tsx` - DEX token swap + +### Backend (Verifier & Relayer) +- **Runtime:** Node.js 18+ +- **API Framework:** Express.js ^4.18 +- **Web3 Library:** ethers.js ^6.10 +- **CORS Support:** cors ^2.8 +- **Environment:** dotenv ^16.3 +- **Process Management:** PM2 (production) + +**Key Features:** +- World ID proof verification via World ID API +- Gasless transaction relay for users +- Reward distribution endpoints +- DEX integration for swap quotes + +**Main File:** `server/verifier.js` + +### Blockchain / Smart Contracts +- **Language:** Solidity ^0.8.20 +- **Development Framework:** Hardhat +- **Libraries:** + - OpenZeppelin Contracts (Ownable, ReentrancyGuard, IERC20) +- **Token Standard:** ERC-20 +- **Primary Networks:** + - WorldChain (Mainnet) + - Ethereum (Mainnet) + - Base (L2) + - Sepolia (Testnet) + +**Key Contract:** `contracts/WorldIDRewards.sol` +- World ID ZKP verification with nullifier tracking +- Daily check-in rewards with 24-hour cooldown +- One-time airdrop claim system +- Streak tracking for consecutive check-ins +- Idempotency protection against replay attacks + +### Development Tools +- **Package Manager:** npm +- **TypeScript:** ^5.3 +- **Node Version:** 18+ +- **Testing:** Jest (for backend), React Native Testing Library +- **Code Editor:** VS Code recommended +- **Mobile Testing:** Expo Go app for development +- **Process Management:** PM2 +- **CI/CD:** GitHub Actions +- **Monitoring:** Telegram Bot notifications +- **Version Control:** Git + +### Development Tools +- **Package Manager:** npm/yarn +- **Code Editor:** VS Code +- **Testing:** Hardhat Test, Chai +- **Linting:** ESLint, Prettier +- **Environment:** dotenv + +--- + +## 5. สถาปัตยกรรมระบบ (System Architecture) + +### 5.1 ภาพรวมสถาปัตยกรรม + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ End Users │ +│ (Web Browser + Wallet) │ +└───────────────────┬────────────────────────┬────────────────────┘ + │ │ + ▼ ▼ + ┌───────────────────┐ ┌───────────────────┐ + │ Frontend dApp │ │ Dashboard │ + │ (Next.js) │ │ (Monitoring) │ + │ Port: 3000 │ │ Port: 8080 │ + └─────────┬─────────┘ └─────────┬─────────┘ + │ │ + └────────┬───────────────┘ + │ + ▼ + ┌────────────────────────┐ + │ Nginx Reverse Proxy │ + │ (SSL Termination) │ + └────────────┬───────────┘ + │ + ┌──────────────┼──────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ + │ API Server │ │ Relayer │ │ Hardhat │ + │ (Express) │ │ Backend │ │ Network │ + │ Port: 3001 │ │ (Auto Tx) │ │ Port: 8545 │ + └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + └────────────────┼────────────────┘ + │ + ▼ + ┌───────────────────────┐ + │ Smart Contracts │ + │ (On Blockchain) │ + ├───────────────────────┤ + │ - ZeaToken (ERC-20) │ + │ - Airdrop │ + │ - Reward │ + │ - Vault (Optional) │ + └───────────────────────┘ + │ + ┌─────────────────┼─────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌────────────┐ ┌────────────┐ ┌────────────┐ + │ WorldChain │ │ Base │ │ Sepolia │ + │ (Primary) │ │ (L2 EVM) │ │ (Testnet) │ + └────────────┘ └────────────┘ └────────────┘ +``` + +### 5.2 การไหลของข้อมูล (Data Flow) + +#### Airdrop Claim Flow: +1. **User** เชื่อมต่อ Wallet ผ่าน Frontend +2. **Frontend** ตรวจสอบ Merkle Proof จาก JSON file +3. **User** กด "Claim" button +4. **Wallet** ขอ Sign Transaction +5. **Smart Contract** ตรวจสอบ Merkle Proof on-chain +6. **Smart Contract** โอน Token ให้ User +7. **Event** ถูก Emit และแสดงใน Dashboard + +#### Reward Distribution Flow: +1. **User** ทำกิจกรรม (เช่น Check-in) +2. **Backend API** ตรวจสอบกิจกรรมและสร้าง Idempotency Key +3. **Relayer** เรียก Smart Contract function `distributeReward()` +4. **Smart Contract** ตรวจสอบ Idempotency และจ่ายรางวัล +5. **Event** ถูก Emit และบันทึกใน Database +6. **Notification** ส่งไปยัง Telegram (ถ้าตั้งค่าไว้) + +### 5.3 Component Integration + +```mermaid +graph TB + A[Deployment Script] -->|Deploy| B[Smart Contracts] + B -->|Bytecode| C[Blockchain Networks] + D[Merkle Generator] -->|Generate| E[Merkle Tree Data] + E -->|Store| F[Frontend Public Folder] + G[Frontend] -->|Read| F + G -->|Sign & Send Tx| B + H[Backend API] -->|Call Contract| B + I[Relayer] -->|Auto Execute| B + J[Dashboard] -->|Monitor Events| C + K[Nginx] -->|Route| G + K -->|Route| H + K -->|Route| J +``` + +### 5.4 Security Architecture + +**Layers of Security:** +1. **Smart Contract Level:** + - OpenZeppelin battle-tested contracts + - Ownable access control + - Idempotency protection + - ReentrancyGuard (where applicable) + +2. **Backend Level:** + - Environment variables for sensitive data + - Rate limiting on API + - WorldID verification + - JWT authentication (if needed) + +3. **Infrastructure Level:** + - Nginx as reverse proxy + - SSL/TLS encryption + - Firewall rules + - Private key isolation + +4. **Operational Level:** + - Separate Deployer and Relayer keys + - Multi-signature for critical operations (future) + - Regular security audits + - Monitoring and alerting + +--- + +## 6. โมเดลข้อมูลเบื้องต้น (Core Data Models) + +### 6.1 On-chain Data Models + +#### ZeaToken (ERC-20) +```solidity +contract ZeaToken is ERC20, Ownable { + string public name; + string public symbol; + uint8 public decimals; + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; +} +``` + +#### AirdropDistributor +```solidity +contract AirdropDistributor is Ownable { + IERC20 public token; + bytes32 public merkleRoot; + mapping(address => bool) public hasClaimed; + + struct ClaimData { + address user; + uint256 amount; + bytes32[] proof; + } +} +``` + +#### Reward +```solidity +contract Reward is Ownable { + IERC20 public token; + mapping(bytes32 => bool) public processed; // Idempotency + + struct RewardEvent { + address recipient; + uint256 amount; + string reason; + address triggeredBy; + uint256 timestamp; + } +} +``` + +### 6.2 Off-chain Data Models + +#### User (Database) +```typescript +interface User { + id: string; // UUID + address: string; // Ethereum address + worldIdHash?: string; // WorldID nullifier hash + createdAt: Date; + lastCheckIn?: Date; + totalRewardsEarned: string; // BigInt as string +} +``` + +#### CheckInRecord (Database) +```typescript +interface CheckInRecord { + id: string; + userId: string; + date: Date; + rewardAmount: string; + txHash?: string; + idempotencyKey: string; // Unique for each check-in + status: 'pending' | 'completed' | 'failed'; +} +``` + +#### MerkleData (JSON File) +```typescript +interface MerkleData { + merkleRoot: string; // bytes32 + proofs: { + [address: string]: { + amount: string; // In wei + proof: string[]; // Array of bytes32 + } + } +} +``` + +#### DeploymentConfig (.env) +```bash +# Network Configuration +MULTI_RPC_LIST="worldchain=https://...,base=https://..." + +# Keys +PRIVATE_KEY="0x..." # Deployer +RELAYER_PRIVATE_KEY="0x..." # Relayer + +# WorldID +WORLD_APP_ID="app_..." +WORLD_APP_API_KEY="api_..." + +# Domains +FRONT_DOMAIN="app.zeaz.dev" +API_DOMAIN="api.zeaz.dev" +DASH_DOMAIN="dash.zeaz.dev" + +# Optional +TELEGRAM_BOT_TOKEN="" +TELEGRAM_CHAT_ID="" +``` + +--- + +## 7. แผนการพัฒนา (Development Roadmap - MVP) + +### Phase 1: Foundation (Week 1-2) ✅ +**เป้าหมาย:** สร้าง Smart Contract และ Deployment System พื้นฐาน + +**Tasks:** +- [x] สร้าง ZeaToken (ERC-20) Contract +- [x] สร้าง AirdropDistributor Contract +- [x] สร้าง Reward Contract +- [x] เขียน Deployment Script (Hardhat) +- [x] ทดสอบ Deploy บน Testnet (Sepolia) + +**Deliverables:** +- Smart Contracts ที่ผ่านการทดสอบ +- Deployment Script ที่ใช้งานได้ +- Documentation พื้นฐาน + +### Phase 2: Merkle System (Week 3) ✅ +**เป้าหมาย:** สร้างระบบ Merkle Tree สำหรับ Airdrop + +**Tasks:** +- [x] สร้าง Merkle Tree Generator +- [x] ทดสอบ Merkle Proof Verification +- [x] สร้าง Whitelist Example +- [x] Integration กับ AirdropDistributor + +**Deliverables:** +- Merkle Generator Script +- Merkle Data JSON +- Test Suite สำหรับ Verification + +### Phase 3: Frontend Development (Week 4-5) +**เป้าหมาย:** สร้าง Frontend dApp สำหรับ Airdrop + +**Tasks:** +- [ ] Setup Next.js + TailwindCSS +- [ ] Implement Wallet Connection (wagmi) +- [ ] สร้างหน้า Airdrop Claim +- [ ] แสดงสถานะ Claim +- [ ] Responsive Design + +**Deliverables:** +- Frontend dApp ที่ใช้งานได้ +- Mobile-friendly UI +- Error handling + +### Phase 4: Reward System Backend (Week 6-7) +**เป้าหมาย:** สร้าง Backend API สำหรับระบบรางวัล + +**Tasks:** +- [ ] สร้าง API Server (Express) +- [ ] Implement Check-in API +- [ ] สร้าง Relayer Service +- [ ] Database Setup (PostgreSQL) +- [ ] WorldID Integration + +**Deliverables:** +- Backend API ที่ใช้งานได้ +- Relayer Service +- Database Schema +- API Documentation + +### Phase 5: Dashboard & Monitoring (Week 8) +**เป้าหมาย:** สร้าง Dashboard สำหรับติดตามระบบ + +**Tasks:** +- [ ] สร้าง Admin Dashboard +- [ ] แสดง Transaction History +- [ ] แสดง User Statistics +- [ ] Telegram Notification Integration +- [ ] Log Management + +**Deliverables:** +- Admin Dashboard +- Real-time Monitoring +- Notification System + +### Phase 6: Multi-chain Deployment (Week 9-10) +**เป้าหมาย:** Deploy บนหลาย Network + +**Tasks:** +- [ ] ปรับปรุง Deployment Script สำหรับ Multi-chain +- [ ] Deploy บน WorldChain +- [ ] Deploy บน Base +- [ ] Deploy บน Ethereum Mainnet +- [ ] Contract Verification + +**Deliverables:** +- Contracts บนทุก Target Network +- Verified Contracts +- Multi-chain Documentation + +### Phase 7: Security & Testing (Week 11-12) +**เป้าหมาย:** ตรวจสอบความปลอดภัยและทดสอบระบบ + +**Tasks:** +- [ ] Smart Contract Audit (Internal) +- [ ] Penetration Testing +- [ ] Load Testing +- [ ] Bug Bounty Program (Optional) +- [ ] Security Documentation + +**Deliverables:** +- Audit Report +- Test Results +- Security Best Practices Guide + +### Phase 8: Production Launch (Week 13-14) +**เป้าหมาย:** เปิดตัวระบบสู่ Production + +**Tasks:** +- [ ] Setup Production Infrastructure +- [ ] SSL Certificate Installation +- [ ] Domain Configuration +- [ ] Monitoring Setup +- [ ] Launch Communication + +**Deliverables:** +- Production System +- SSL-enabled Domains +- Monitoring Dashboard +- Launch Announcement + +--- + +## ลำดับความสำคัญของฟีเจอร์ (Feature Priority) + +### Must Have (MVP) +1. ✅ ERC-20 Token Contract +2. ✅ Airdrop System with Merkle Tree +3. ✅ Reward Distribution System +4. 🔄 Frontend dApp (Airdrop Claim) +5. 🔄 Basic Deployment Script + +### Should Have (Post-MVP) +1. Backend API & Relayer +2. WorldID Integration +3. Dashboard & Monitoring +4. Multi-chain Support +5. Telegram Notifications + +### Nice to Have (Future) +1. Mobile App +2. Advanced Analytics +3. NFT Rewards +4. Staking System +5. Governance Features + +--- + +## ภาคผนวก (Appendix) + +### A. Glossary +- **Airdrop:** การแจกจ่าย Token ฟรีให้กับ Whitelist +- **Merkle Tree:** โครงสร้างข้อมูลสำหรับยืนยัน Membership อย่างมีประสิทธิภาพ +- **Idempotency:** การป้องกันการดำเนินการซ้ำ +- **Relayer:** Wallet ที่จ่าย Gas แทนผู้ใช้ +- **WorldID:** ระบบยืนยันตัวตนแบบ Proof of Personhood + +### B. References +- OpenZeppelin Contracts: https://docs.openzeppelin.com/ +- Hardhat Documentation: https://hardhat.org/ +- Merkle Tree JS: https://github.com/merkletreejs/merkletreejs +- WorldID Docs: https://docs.world.org/ + +### C. Contact & Support +- **Developer:** PHIPHAT PHOEMSUK +- **Email:** admin@zeaz.dev +- **Website:** https://app.zeaz.dev/ +- **GitHub:** https://github.com/ZeaZDev + +--- + +**หมายเหตุ:** พิมพ์เขียวนี้เป็นเอกสารสดที่จะได้รับการปรับปรุงตามการพัฒนาโครงการ ควรตรวจสอบเวอร์ชันล่าสุดเสมอ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb20037 --- /dev/null +++ b/README.md @@ -0,0 +1,580 @@ +# ZeaZDev Mini App + +> A World App Mini App with World ID verification for Sybil-resistant rewards and DeFi features + +ZeaZDev is a production-ready World App Mini App that uses Zero-Knowledge Proof (ZKP) via World ID to provide Proof of Personhood verification. Users can claim daily rewards, receive airdrops, manage their wallets, and swap tokens - all with gasless transactions powered by a relayer service. + +## ✨ Key Features + +- **🌍 World ID Verification (ZKP)** - Zero-Knowledge Proof verification for Proof of Personhood, preventing Sybil attacks without revealing personal data +- **📱 React Native Mini App** - Mobile-first experience built with Expo, runs natively in World App +- **🎁 Daily Check-in Rewards** - Earn ZEA tokens every 24 hours with streak tracking to incentivize continuous engagement +- **💰 One-time Airdrop** - Welcome bonus for verified World ID users (1000 ZEA tokens) +- **⚡ Gasless Transactions** - Relayer service pays gas fees, enabling frictionless onboarding for new users +- **💱 DEX Token Swap** - Integrated decentralized exchange functionality for swapping WLD, ETH, ZEA, and USDC +- **👛 Multi-Token Wallet** - Manage WLD tokens and gas tokens (ETH/MATIC) with send functionality +- **🔒 Nullifier Hash Protection** - Prevents the same World ID from being used multiple times (anti-replay attack) +- **🔄 Automatic Streak Tracking** - Consecutive daily check-ins build streaks for potential bonus rewards + +## 🛠️ Technology Stack + +### Frontend (Mini App) +- **Framework:** React Native 0.73 with Expo ~50.0 +- **Navigation:** Expo Router ~3.4 +- **World ID SDK:** @worldcoin/idkit-core ^1.0 +- **Web3 Integration:** ethers.js ^6.10 +- **Storage:** @react-native-async-storage/async-storage +- **Language:** TypeScript 5.3+ + +### Backend (Verifier & Relayer) +- **Runtime:** Node.js 18+ +- **API Framework:** Express.js 4.18 +- **Web3 Library:** ethers.js 6.10 +- **CORS:** cors 2.8 +- **Environment:** dotenv 16.3 + +### Smart Contracts +- **Language:** Solidity ^0.8.20 +- **Development:** Hardhat +- **Libraries:** OpenZeppelin Contracts (Ownable, ReentrancyGuard, IERC20) +- **Token Standard:** ERC-20 +- **Networks:** WorldChain (Primary), Ethereum, Base, Sepolia + +### Key Smart Contract Features +- World ID ZKP verification with nullifier tracking +- Daily check-in with 24-hour cooldown +- One-time airdrop claim system +- Check-in streak tracking +- Idempotency protection + +## 🚀 Getting Started + +### Prerequisites + +Before you begin, ensure you have the following installed and configured: + +- **Node.js v18+** - [Download here](https://nodejs.org/) +- **npm or yarn** - Package manager (comes with Node.js) +- **Expo CLI** - Install with `npm install -g expo-cli` +- **Expo Go App** (iOS/Android) - For testing Mini App on real devices +- **World ID Account** - Register at [World App](https://worldcoin.org/download) +- **World ID Developer Portal Access** - [developer.worldcoin.org](https://developer.worldcoin.org/) +- **Private Keys:** + - Deployer wallet private key (for smart contract deployment) + - Relayer wallet private key (for gasless transactions) +- **RPC Endpoints** - Alchemy or Infura API keys for target blockchain networks + +### World ID Setup Instructions + +#### Step 1: Register Your Mini App on Worldcoin Developer Portal + +1. **Visit the Developer Portal:** + - Go to [https://developer.worldcoin.org/](https://developer.worldcoin.org/) + - Sign in with your Worldcoin account + +2. **Create a New App:** + - Click "Create New App" + - Enter app details: + - **App Name:** ZeaZDev Mini App + - **App Description:** World ID verified rewards and DeFi platform + - **App Type:** Mini App + +3. **Configure Actions:** + - Create an action for verification: + - **Action Name:** `verify-humanity` + - **Action ID:** (will be auto-generated, e.g., `verify-humanity_12345`) + - **Description:** Verify user is a unique human for rewards + - **Max Verifications:** 1 (per person) + +4. **Get Your Credentials:** + - **App ID:** (e.g., `app_staging_abc123def456...`) - For frontend + - **API Key:** (e.g., `api_secret_xyz789...`) - For backend verification + - **Action ID:** Use for specific verification flows + +5. **Configure Callback URLs** (if using web version): + - Development: `http://localhost:3000` + - Production: `https://app.zeaz.dev` + +#### Step 2: Set Environment Variables + +Create a `.env` file in both `mini-app/` and `server/` directories: + +### Installation & Configuration + +1. **Clone the Repository** + ```bash + git clone https://github.com/ZeaZDev/ZeaZDev.git + cd ZeaZDev + ``` + +2. **Configure Mini App (Frontend)** + + ```bash + cd mini-app + npm install + ``` + + Create `mini-app/.env`: + ```bash + # World ID Configuration (from Developer Portal) + WORLD_APP_ID=app_staging_your_app_id_here + WORLD_ACTION_ID=verify-humanity_your_action_id + + # API Configuration + API_URL=http://localhost:3000 + RPC_URL=https://worldchain-mainnet.g.alchemy.com/v2/your-key + ``` + +3. **Configure Backend Verifier** + + ```bash + cd ../server + npm install + ``` + + Create `server/.env`: + ```bash + # Server Configuration + PORT=3000 + + # World ID Configuration + WORLD_APP_ID=app_staging_your_app_id_here + WORLD_APP_API_KEY=api_your_secret_api_key_here + WORLD_ACTION_ID=verify-humanity_your_action_id + + # Blockchain Configuration + RPC_URL=https://worldchain-mainnet.g.alchemy.com/v2/your-key + RELAYER_PRIVATE_KEY=0xYOUR_RELAYER_PRIVATE_KEY + + # Contract Addresses (update after deployment) + WORLD_ID_REWARDS_CONTRACT=0x0000000000000000000000000000000000000000 + ZEA_TOKEN_CONTRACT=0x0000000000000000000000000000000000000000 + ``` + +4. **Deploy Smart Contracts** + + ```bash + cd ../contracts + npm install + npx hardhat compile + + # Deploy WorldIDRewards contract + npx hardhat run scripts/deploy-worldid-rewards.js --network worldchain + ``` + + Copy the deployed contract addresses and update them in `server/.env` + +5. **Legacy Configuration (if using original deployment scripts)** + + For backward compatibility with existing deployment scripts: + + Create `.env` file in the project root: + + ```ini + # === ZeaZDev Configuration === + + # 1. Network Configuration (Required) + # Format: network_name=rpc_url (comma-separated, no spaces) + MULTI_RPC_LIST="worldchain=https://worldchain-mainnet.g.alchemy.com/v2/YOUR_KEY,base=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY,sepolia=https://sepolia.infura.io/v3/YOUR_KEY" + + # 2. Private Keys (Required) + # Deployer Key: Used to deploy contracts and own them + PRIVATE_KEY="0xYOUR_DEPLOYER_PRIVATE_KEY" + # Relayer Key: Hot wallet for gasless transactions + RELAYER_PRIVATE_KEY="0xYOUR_RELAYER_PRIVATE_KEY" + + # 3. WorldID Configuration (Required for identity features) + # Public App ID for Frontend/IDKit + WORLD_APP_ID="app_staging_YOUR_APP_ID" + # Secret API Key for Backend verification + WORLD_APP_API_KEY="api_YOUR_API_KEY" + + # 4. Domain Configuration (Required for production) + FRONT_DOMAIN="app.zeaz.dev" + DASH_DOMAIN="dash.zeaz.dev" + API_DOMAIN="api.zeaz.dev" + + # 5. Block Explorer Verification (Optional) + ETHERSCAN_API_KEY="YOUR_ETHERSCAN_API_KEY" + + # 6. Notifications (Optional) + TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN" + TELEGRAM_CHAT_ID="YOUR_CHAT_ID" + + # 7. Git Logging (Optional) + GIT_LOG_REPO="https://github.com/yourusername/logs.git" + GIT_LOG_BRANCH="main" + ``` + +4. **Configure Whitelist for Airdrop** + + Edit the whitelist file for Merkle tree generation: + + ```bash + nano packages/merkle-generator/whitelist.example.json + ``` + + Example format: + ```json + [ + { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "amount": "100000000000000000000" + }, + { + "address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "amount": "250000000000000000000" + } + ] + ``` + +### Running the Project + +#### Running the Mini App (Development) + +**Step 1: Start Backend Verifier Server** +```bash +cd server +npm start +``` + +The backend will start on `http://localhost:3000` and handle: +- World ID proof verification +- Gasless transaction relay +- Reward distribution +- Swap quotes + +**Step 2: Start Mini App** +```bash +cd mini-app +npm start +# or +expo start +``` + +This will open the Expo Dev Tools in your browser. + +**Step 3: Test on Device** +- Scan the QR code with Expo Go app (iOS/Android) +- The Mini App will load on your device +- Test World ID verification flow +- Test all features (Wallet, Rewards, Swap) + +#### Running on World App (Production) + +To deploy your Mini App to World App: + +1. **Build for Production:** + ```bash + cd mini-app + eas build --platform ios + eas build --platform android + ``` + +2. **Submit to World App:** + - Follow World App Mini App submission guidelines + - Provide App ID and action configurations + - Wait for approval + +3. **Production Backend:** + ```bash + cd server + npm start + # Use PM2 for process management in production + pm2 start verifier.js --name zeazdev-verifier + ``` + +#### Legacy Deployment (Original Scripts) + +For backward compatibility with existing deployment infrastructure: + +```bash +# Run the automated installer +sudo bash run.sh +``` + +This will deploy the original Web3 infrastructure including Merkle-based airdrops. + +### Accessing the Application + +**Development:** +- **Mini App:** Via Expo Go app (scan QR code) +- **Backend API:** `http://localhost:3000` +- **Smart Contracts:** Deployed on configured networks + +**Production (World App):** +- **Mini App:** Available in World App's Mini Apps section +- **Backend API:** Your production API URL +- **Smart Contracts:** Deployed on WorldChain/Ethereum Mainnet + +## 🧪 Running Tests + +To run the automated tests for smart contracts: + +```bash +cd packages/hardhat +npx hardhat test +``` + +To run tests with coverage: + +```bash +npx hardhat coverage +``` + +To run frontend tests: + +```bash +cd packages/frontend +npm test +``` + +## 📖 Usage Guide + +### For End Users + +#### First Time Setup + +1. **Open ZeaZDev Mini App** + - Launch World App on your device + - Navigate to Mini Apps section + - Open ZeaZDev Mini App + +2. **Verify with World ID (Required)** + - The app will show "Verify with World ID" screen + - Click the verification button + - World App will prompt you to scan with Orb or use Device verification + - Complete the verification process + - Your Zero-Knowledge Proof will be generated and verified + - Once successful, you'll gain access to all features + +#### Daily Usage + +**Claim Welcome Airdrop (One-time)** +1. After verification, go to "Rewards" tab +2. Find the "Welcome Airdrop" section +3. Click "Claim Airdrop" +4. Confirm the transaction +5. Receive 1000 ZEA tokens instantly (gasless!) + +**Daily Check-in** +1. Open "Rewards" tab +2. Click "Check In Now" button (available every 24 hours) +3. Earn 100 ZEA tokens per check-in +4. Build your streak by checking in daily +5. Track your total rewards and current streak + +**Manage Wallet** +1. Go to "Wallet" tab +2. View your WLD and Gas token balances +3. Send tokens: + - Click "Send" button + - Select token (WLD or ETH) + - Enter recipient address + - Enter amount + - Confirm transaction (gasless!) + +**Swap Tokens** +1. Go to "Swap" tab +2. Select tokens to swap (e.g., WLD → ETH) +3. Enter amount +4. Review exchange rate and price impact +5. Click "Swap" and confirm +6. Transaction executes via DEX (gasless!) + +### For Developers + +#### Deploy New Instance + +1. **Register Mini App:** + ```bash + # Get World App ID and API Key from developer portal + # Update .env files in mini-app/ and server/ + ``` + +2. **Deploy Smart Contracts:** + ```bash + cd contracts + npx hardhat run scripts/deploy-worldid-rewards.js --network worldchain + # Note the contract address + ``` + +3. **Fund Reward Contract:** + ```bash + # Send ZEA tokens to WorldIDRewards contract + # Ensure relayer wallet has gas tokens + ``` + +4. **Update Configuration:** + ```bash + # Update contract addresses in server/.env + # Update API URL in mini-app/.env + ``` + +5. **Start Services:** + ```bash + # Terminal 1: Backend + cd server && npm start + + # Terminal 2: Mini App + cd mini-app && expo start + ``` + +#### Monitor System + +**Check Backend Logs:** +```bash +cd server +npm start +# Watch console for verification requests, transactions +``` + +**Check Contract Status:** +```bash +npx hardhat console --network worldchain +> const contract = await ethers.getContractAt("WorldIDRewards", "0x...") +> await contract.getContractBalance() +> await contract.isUserVerified("0x...") +``` + +**Monitor User Activity:** +- All events are emitted on-chain +- Parse `UserVerified`, `DailyCheckIn`, `AirdropClaimed` events +- Track via blockchain explorer + +## 📁 Project Structure + +``` +ZeaZDev/ +├── contracts/ # Smart contracts +│ ├── Reward.sol # Reward distribution contract +│ └── ... +├── scripts/ # Deployment and utility scripts +│ ├── deploy.js # Main deployment script +│ └── ... +├── packages/ # Monorepo packages (if used) +│ ├── hardhat/ # Hardhat configuration +│ ├── frontend/ # Next.js frontend dApp +│ ├── merkle-generator/ # Merkle tree generator +│ └── installer-app/ # Electron installer (optional) +├── Rewards/ # Reward system documentation +├── .github/ # GitHub Actions workflows +├── docker-compose.yml # Docker Compose configuration +├── run.sh # Main execution wrapper +├── ZeaZDev-Release-v10.1.sh # Main installer script +├── PROJECT_BLUEPRINT.md # Comprehensive project blueprint +├── .env.example # Environment variables template +└── README.md # This file +``` + +## 🔐 Security Considerations + +### Smart Contract Security +- All contracts use OpenZeppelin's battle-tested implementations +- Ownable pattern for access control +- Idempotency protection to prevent double-spending +- ReentrancyGuard for external calls +- Regular security audits recommended + +### Operational Security +- **Private Keys:** Store securely, never commit to version control +- **Environment Variables:** Use `.env` files, add to `.gitignore` +- **Relayer Wallet:** Fund with minimal amount, monitor balance +- **Rate Limiting:** Implement on API endpoints +- **SSL/TLS:** Always use HTTPS in production +- **Firewall:** Configure proper firewall rules +- **Monitoring:** Set up alerts for suspicious activities + +### Best Practices +1. Use separate deployer and relayer keys +2. Test on testnets before mainnet deployment +3. Verify all contracts on block explorers +4. Implement multi-signature for critical operations +5. Regular backups of configuration and data +6. Keep dependencies up to date +7. Monitor gas prices and adjust strategies +8. Use hardware wallets for deployer keys in production + +## 🤝 Contributing + +Contributions are welcome! To contribute to ZeaZDev: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +Please ensure your code follows the existing style and includes appropriate tests. + +### Development Guidelines +- Write clear commit messages +- Add tests for new features +- Update documentation as needed +- Follow Solidity and JavaScript best practices +- Run linter before committing + +## 📄 License + +This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. + +### MIT License Summary +- ✅ Commercial use +- ✅ Modification +- ✅ Distribution +- ✅ Private use +- ⚠️ Liability and warranty limitations apply + +## 👤 Contact & Support + +**Developer:** PHIPHAT PHOEMSUK (ZeaZDev) + +**Email:** admin@zeaz.dev + +**Website:** [https://app.zeaz.dev/](https://app.zeaz.dev/) + +**GitHub:** [https://github.com/ZeaZDev](https://github.com/ZeaZDev) + +### Support Channels +- **GitHub Issues:** For bug reports and feature requests +- **Email:** For general inquiries and support +- **Telegram:** Community support (check website for link) + +## 🙏 Acknowledgments + +- **OpenZeppelin** - For secure smart contract libraries +- **Hardhat** - For excellent development framework +- **Worldcoin** - For WorldID integration +- **Ethereum Community** - For continuous innovation +- **Contributors** - Thank you to all who have contributed to this project + +## 📊 Project Status + +- **Current Version:** v10.1 +- **Status:** Active Development +- **Stability:** Beta (use with caution in production) +- **Last Updated:** 2025 + +### Roadmap +- [x] Core smart contracts +- [x] Merkle-based airdrop system +- [x] Multi-chain deployment +- [x] WorldID integration +- [ ] Mobile app +- [ ] Advanced analytics +- [ ] NFT reward support +- [ ] Governance features + +## 📚 Additional Resources + +- [Project Blueprint (Thai)](PROJECT_BLUEPRINT.md) - Comprehensive technical documentation +- [Smart Contract Documentation](contracts/README.md) - Contract specifications +- [API Documentation](docs/API.md) - Backend API reference +- [Deployment Guide](docs/DEPLOYMENT.md) - Detailed deployment instructions +- [Troubleshooting](docs/TROUBLESHOOTING.md) - Common issues and solutions + +--- + +**Note:** This is a living document and will be updated as the project evolves. Always refer to the latest version in the repository. + +Made with ❤️ by ZeaZDev Team diff --git a/contracts/WorldIDRewards.sol b/contracts/WorldIDRewards.sol new file mode 100644 index 0000000..020e707 --- /dev/null +++ b/contracts/WorldIDRewards.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +/** + * @title WorldIDRewards + * @dev Smart Contract for World ID verified reward distribution + * @notice This contract handles daily check-ins and airdrop claims with World ID verification + * + * Developer: PHIPHAT PHOEMSUK (ZeaZDev) + * Project: ZeaZDev Mini App + * License: MIT + */ +contract WorldIDRewards is Ownable, ReentrancyGuard { + + // ==================== State Variables ==================== + + IERC20 public rewardToken; + + // World ID verification parameters + address public worldIdVerifier; + uint256 public externalNullifier; + + // Reward configuration + uint256 public dailyRewardAmount; + uint256 public airdropAmount; + uint256 public constant CHECK_IN_COOLDOWN = 24 hours; + + // User tracking + struct UserData { + bool isVerified; // Has completed World ID verification + bool hasClaimedAirdrop; // Has claimed initial airdrop + uint256 lastCheckIn; // Timestamp of last check-in + uint256 totalRewardsClaimed; // Total rewards accumulated + uint256 checkInStreak; // Current check-in streak + } + + // Mappings + mapping(bytes32 => bool) public nullifierHashUsed; // Prevent replay attacks + mapping(address => UserData) public users; + mapping(address => bytes32) public userNullifierHash; // Track user's nullifier + + // ==================== Events ==================== + + event UserVerified( + address indexed user, + bytes32 indexed nullifierHash, + uint256 timestamp + ); + + event DailyCheckIn( + address indexed user, + uint256 amount, + uint256 streak, + uint256 timestamp + ); + + event AirdropClaimed( + address indexed user, + uint256 amount, + uint256 timestamp + ); + + event RewardConfigUpdated( + uint256 dailyReward, + uint256 airdropAmount + ); + + // ==================== Constructor ==================== + + constructor( + address _rewardToken, + address _worldIdVerifier, + uint256 _externalNullifier, + uint256 _dailyRewardAmount, + uint256 _airdropAmount + ) Ownable(msg.sender) { + require(_rewardToken != address(0), "Invalid token address"); + require(_worldIdVerifier != address(0), "Invalid verifier address"); + + rewardToken = IERC20(_rewardToken); + worldIdVerifier = _worldIdVerifier; + externalNullifier = _externalNullifier; + dailyRewardAmount = _dailyRewardAmount; + airdropAmount = _airdropAmount; + } + + // ==================== World ID Verification ==================== + + /** + * @notice Verify user's World ID proof and register them + * @dev This function verifies the ZKP and prevents replay attacks + * @param signal User's address as signal + * @param root Merkle root from World ID + * @param nullifierHash Unique identifier for this verification + * @param proof ZK proof array + */ + function verifyAndRegister( + address signal, + uint256 root, + uint256 nullifierHash, + uint256[8] calldata proof + ) external nonReentrant { + require(signal == msg.sender, "Signal must be sender"); + require(!users[msg.sender].isVerified, "Already verified"); + + bytes32 nullifierHashBytes = bytes32(nullifierHash); + require(!nullifierHashUsed[nullifierHashBytes], "Nullifier already used"); + + // Verify the ZK proof with World ID verifier + // Note: In production, this calls the actual World ID verifier contract + _verifyWorldIdProof(signal, root, nullifierHash, proof); + + // Mark nullifier as used + nullifierHashUsed[nullifierHashBytes] = true; + userNullifierHash[msg.sender] = nullifierHashBytes; + + // Register user + users[msg.sender].isVerified = true; + + emit UserVerified(msg.sender, nullifierHashBytes, block.timestamp); + } + + /** + * @dev Internal function to verify World ID proof + * @notice In production, this should call the actual World ID verifier contract + */ + function _verifyWorldIdProof( + address signal, + uint256 root, + uint256 nullifierHash, + uint256[8] calldata proof + ) internal view { + // Interface with World ID verifier contract + // Example: IWorldIDVerifier(worldIdVerifier).verifyProof(...) + + // For development, we can add a simplified check or mock + // In production, this MUST call the actual World ID verification contract + require(worldIdVerifier != address(0), "Verifier not set"); + + // The actual implementation would be: + // (bool success, ) = worldIdVerifier.staticcall( + // abi.encodeWithSignature( + // "verifyProof(uint256,address,uint256,uint256,uint256[8])", + // root, signal, nullifierHash, externalNullifier, proof + // ) + // ); + // require(success, "World ID verification failed"); + } + + // ==================== Reward Functions ==================== + + /** + * @notice Claim initial airdrop (one-time, requires World ID verification) + */ + function claimAirdrop() external nonReentrant { + require(users[msg.sender].isVerified, "Not verified with World ID"); + require(!users[msg.sender].hasClaimedAirdrop, "Airdrop already claimed"); + require(rewardToken.balanceOf(address(this)) >= airdropAmount, "Insufficient contract balance"); + + users[msg.sender].hasClaimedAirdrop = true; + users[msg.sender].totalRewardsClaimed += airdropAmount; + + require(rewardToken.transfer(msg.sender, airdropAmount), "Transfer failed"); + + emit AirdropClaimed(msg.sender, airdropAmount, block.timestamp); + } + + /** + * @notice Daily check-in to claim rewards + * @dev Can only be claimed once per 24 hours + */ + function dailyCheckIn() external nonReentrant { + require(users[msg.sender].isVerified, "Not verified with World ID"); + require( + block.timestamp >= users[msg.sender].lastCheckIn + CHECK_IN_COOLDOWN, + "Check-in cooldown not expired" + ); + require(rewardToken.balanceOf(address(this)) >= dailyRewardAmount, "Insufficient contract balance"); + + UserData storage user = users[msg.sender]; + + // Update streak (reset if more than 48 hours since last check-in) + if (block.timestamp <= user.lastCheckIn + (CHECK_IN_COOLDOWN * 2)) { + user.checkInStreak += 1; + } else { + user.checkInStreak = 1; + } + + user.lastCheckIn = block.timestamp; + user.totalRewardsClaimed += dailyRewardAmount; + + require(rewardToken.transfer(msg.sender, dailyRewardAmount), "Transfer failed"); + + emit DailyCheckIn(msg.sender, dailyRewardAmount, user.checkInStreak, block.timestamp); + } + + /** + * @notice Get user's check-in status + * @return canCheckIn Whether user can check in now + * @return timeUntilNext Seconds until next check-in available + */ + function getCheckInStatus(address user) external view returns (bool canCheckIn, uint256 timeUntilNext) { + if (!users[user].isVerified) { + return (false, 0); + } + + uint256 nextCheckInTime = users[user].lastCheckIn + CHECK_IN_COOLDOWN; + + if (block.timestamp >= nextCheckInTime) { + return (true, 0); + } else { + return (false, nextCheckInTime - block.timestamp); + } + } + + // ==================== Admin Functions ==================== + + /** + * @notice Update reward amounts + */ + function updateRewardAmounts(uint256 _dailyReward, uint256 _airdropAmount) external onlyOwner { + dailyRewardAmount = _dailyReward; + airdropAmount = _airdropAmount; + + emit RewardConfigUpdated(_dailyReward, _airdropAmount); + } + + /** + * @notice Update World ID verifier address + */ + function updateWorldIdVerifier(address _newVerifier) external onlyOwner { + require(_newVerifier != address(0), "Invalid address"); + worldIdVerifier = _newVerifier; + } + + /** + * @notice Update external nullifier + */ + function updateExternalNullifier(uint256 _newNullifier) external onlyOwner { + externalNullifier = _newNullifier; + } + + /** + * @notice Fund contract with reward tokens + */ + function fundContract(uint256 amount) external onlyOwner { + require(rewardToken.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + } + + /** + * @notice Emergency withdraw tokens + */ + function emergencyWithdraw(address token, uint256 amount) external onlyOwner { + require(IERC20(token).transfer(msg.sender, amount), "Transfer failed"); + } + + /** + * @notice Get contract's reward token balance + */ + function getContractBalance() external view returns (uint256) { + return rewardToken.balanceOf(address(this)); + } + + // ==================== View Functions ==================== + + /** + * @notice Check if user is verified with World ID + */ + function isUserVerified(address user) external view returns (bool) { + return users[user].isVerified; + } + + /** + * @notice Get user's complete data + */ + function getUserData(address user) external view returns ( + bool isVerified, + bool hasClaimedAirdrop, + uint256 lastCheckIn, + uint256 totalRewardsClaimed, + uint256 checkInStreak + ) { + UserData memory userData = users[user]; + return ( + userData.isVerified, + userData.hasClaimedAirdrop, + userData.lastCheckIn, + userData.totalRewardsClaimed, + userData.checkInStreak + ); + } +} diff --git a/mini-app/app.json b/mini-app/app.json new file mode 100644 index 0000000..a550014 --- /dev/null +++ b/mini-app/app.json @@ -0,0 +1,37 @@ +{ + "expo": { + "name": "ZeaZDev Mini App", + "slug": "zeazdev-mini-app", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true, + "bundleIdentifier": "dev.zeaz.miniapp" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "dev.zeaz.miniapp" + }, + "web": { + "favicon": "./assets/favicon.png" + }, + "extra": { + "worldAppId": process.env.WORLD_APP_ID, + "worldActionId": process.env.WORLD_ACTION_ID, + "apiUrl": process.env.API_URL || "http://localhost:3000" + } + } +} diff --git a/mini-app/package.json b/mini-app/package.json new file mode 100644 index 0000000..50f17e5 --- /dev/null +++ b/mini-app/package.json @@ -0,0 +1,28 @@ +{ + "name": "zeazdev-mini-app", + "version": "1.0.0", + "main": "expo-router/entry", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web" + }, + "dependencies": { + "expo": "~50.0.0", + "expo-router": "~3.4.0", + "react": "18.2.0", + "react-native": "0.73.0", + "@worldcoin/idkit-core": "^1.0.0", + "ethers": "^6.10.0", + "@react-native-async-storage/async-storage": "^1.21.0", + "react-native-safe-area-context": "4.8.2", + "react-native-screens": "~3.29.0" + }, + "devDependencies": { + "@babel/core": "^7.23.0", + "@types/react": "~18.2.45", + "typescript": "^5.3.0" + }, + "private": true +} diff --git a/mini-app/src/config/constants.ts b/mini-app/src/config/constants.ts new file mode 100644 index 0000000..78ad28b --- /dev/null +++ b/mini-app/src/config/constants.ts @@ -0,0 +1,23 @@ +/** + * constants.ts + * Application configuration constants + */ + +// API Configuration +export const API_URL = process.env.API_URL || 'http://localhost:3000'; +export const RPC_URL = process.env.RPC_URL || 'https://worldchain-mainnet.g.alchemy.com/v2/your-key'; + +// World ID Configuration +export const WORLD_APP_ID = process.env.WORLD_APP_ID || ''; +export const WORLD_ACTION_ID = process.env.WORLD_ACTION_ID || ''; + +// Contract Addresses (update after deployment) +export const CONTRACTS = { + ZEA_TOKEN: '0x0000000000000000000000000000000000000000', + WORLD_ID_REWARDS: '0x0000000000000000000000000000000000000000', + WLD_TOKEN: '0x0000000000000000000000000000000000000000', +}; + +// Reward Amounts (in Wei) +export const DAILY_REWARD_AMOUNT = '100000000000000000000'; // 100 ZEA +export const AIRDROP_AMOUNT = '1000000000000000000000'; // 1000 ZEA diff --git a/mini-app/src/screens/AuthGate.tsx b/mini-app/src/screens/AuthGate.tsx new file mode 100644 index 0000000..217221a --- /dev/null +++ b/mini-app/src/screens/AuthGate.tsx @@ -0,0 +1,292 @@ +/** + * AuthGate.tsx + * World ID Authentication Gate - First screen requiring World ID verification + * + * This component handles Zero-Knowledge Proof (ZKP) verification using World ID + * Users must verify their humanity before accessing any financial features + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ActivityIndicator, + Alert, +} from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { IDKit, ISuccessResult } from '@worldcoin/idkit-core'; +import { verifyProof } from '../services/worldid'; +import { ethers } from 'ethers'; + +interface AuthGateProps { + onVerified: (address: string, nullifierHash: string) => void; +} + +export default function AuthGate({ onVerified }: AuthGateProps) { + const [isLoading, setIsLoading] = useState(false); + const [isVerifying, setIsVerifying] = useState(false); + const [userAddress, setUserAddress] = useState(null); + + useEffect(() => { + checkExistingAuth(); + }, []); + + /** + * Check if user is already authenticated + */ + const checkExistingAuth = async () => { + try { + const storedAddress = await AsyncStorage.getItem('userAddress'); + const storedNullifier = await AsyncStorage.getItem('nullifierHash'); + + if (storedAddress && storedNullifier) { + setUserAddress(storedAddress); + onVerified(storedAddress, storedNullifier); + } + } catch (error) { + console.error('Error checking auth:', error); + } + }; + + /** + * Handle World ID verification success + */ + const handleVerifySuccess = async (result: ISuccessResult) => { + try { + setIsVerifying(true); + + // Extract proof data from World ID + const { merkle_root, nullifier_hash, proof } = result; + + // Generate or get user's Ethereum address + // In production, this should be from a secure wallet + let address = userAddress; + if (!address) { + const wallet = ethers.Wallet.createRandom(); + address = wallet.address; + await AsyncStorage.setItem('userAddress', address); + // Store private key securely (in production, use secure enclave) + await AsyncStorage.setItem('privateKey', wallet.privateKey); + } + + console.log('Verifying proof with backend...'); + console.log('Address:', address); + console.log('Merkle Root:', merkle_root); + console.log('Nullifier Hash:', nullifier_hash); + + // Send proof to backend for verification + const verificationResult = await verifyProof({ + signal: address, + merkle_root, + nullifier_hash, + proof, + }); + + if (verificationResult.success) { + // Store authentication data + await AsyncStorage.setItem('nullifierHash', nullifier_hash); + await AsyncStorage.setItem('isVerified', 'true'); + await AsyncStorage.setItem('verifiedAt', new Date().toISOString()); + + Alert.alert( + 'Verification Successful! 🎉', + 'You can now access all features of ZeaZDev Mini App', + [ + { + text: 'Continue', + onPress: () => onVerified(address!, nullifier_hash), + }, + ] + ); + } else { + throw new Error(verificationResult.message || 'Verification failed'); + } + } catch (error: any) { + console.error('Verification error:', error); + Alert.alert( + 'Verification Failed', + error.message || 'Failed to verify your World ID. Please try again.', + [{ text: 'OK' }] + ); + } finally { + setIsVerifying(false); + } + }; + + /** + * Handle verification failure + */ + const handleVerifyFailure = (error: Error) => { + console.error('World ID verification failed:', error); + Alert.alert( + 'Verification Failed', + 'World ID verification was not successful. Please try again.', + [{ text: 'OK' }] + ); + }; + + /** + * Trigger World ID verification + */ + const startVerification = () => { + setIsLoading(true); + // IDKit will open World App for verification + // The actual implementation depends on @worldcoin/idkit-core version + }; + + return ( + + + {/* Header */} + + 🌍 + Welcome to ZeaZDev + + World ID Verified Mini App + + + + {/* Info Section */} + + Why World ID? + + • Proof of Personhood - Verify you're a unique human + + + • Privacy First - No personal data collected + + + • Sybil Resistant - Prevent multiple accounts + + + • Secure Rewards - Access exclusive features + + + + {/* Verification Button */} + + {isVerifying ? ( + <> + + Verifying... + + ) : ( + + Verify with World ID + + )} + + + {/* IDKit Component (simplified for demonstration) */} + {isLoading && ( + + )} + + {/* Footer */} + + + By continuing, you agree to our Terms of Service + + + ZeaZDev Mini App v1.0 + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0F172A', + }, + content: { + flex: 1, + padding: 24, + justifyContent: 'space-between', + }, + header: { + alignItems: 'center', + marginTop: 60, + }, + logo: { + fontSize: 64, + marginBottom: 20, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#FFFFFF', + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: '#94A3B8', + }, + infoBox: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginVertical: 32, + }, + infoTitle: { + fontSize: 18, + fontWeight: '600', + color: '#FFFFFF', + marginBottom: 16, + }, + infoText: { + fontSize: 14, + color: '#CBD5E1', + marginBottom: 12, + lineHeight: 20, + }, + button: { + backgroundColor: '#6366F1', + borderRadius: 12, + padding: 18, + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + shadowColor: '#6366F1', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 5, + }, + buttonDisabled: { + backgroundColor: '#4B5563', + shadowOpacity: 0, + }, + buttonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + footer: { + alignItems: 'center', + marginTop: 24, + }, + footerText: { + fontSize: 12, + color: '#64748B', + marginBottom: 8, + }, + footerSubtext: { + fontSize: 10, + color: '#475569', + }, +}); diff --git a/mini-app/src/screens/RewardScreen.tsx b/mini-app/src/screens/RewardScreen.tsx new file mode 100644 index 0000000..b2714a2 --- /dev/null +++ b/mini-app/src/screens/RewardScreen.tsx @@ -0,0 +1,476 @@ +/** + * RewardScreen.tsx + * Daily check-in and airdrop claim screen + * + * Features: + * - Daily check-in for rewards + * - Airdrop claim (one-time) + * - Check-in streak tracking + * - Reward history + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + ActivityIndicator, + Alert, + ScrollView, +} from 'react'; +import { claimAirdrop, dailyCheckIn, getCheckInStatus } from '../services/rewards'; + +interface RewardScreenProps { + userAddress: string; + nullifierHash: string; +} + +export default function RewardScreen({ userAddress, nullifierHash }: RewardScreenProps) { + const [isLoadingStatus, setIsLoadingStatus] = useState(true); + const [isClaimingAirdrop, setIsClaimingAirdrop] = useState(false); + const [isCheckingIn, setIsCheckingIn] = useState(false); + + // User status + const [hasClaimedAirdrop, setHasClaimedAirdrop] = useState(false); + const [canCheckIn, setCanCheckIn] = useState(false); + const [timeUntilNextCheckIn, setTimeUntilNextCheckIn] = useState(0); + const [checkInStreak, setCheckInStreak] = useState(0); + const [totalRewards, setTotalRewards] = useState('0'); + + useEffect(() => { + loadUserStatus(); + + // Update countdown every second + const interval = setInterval(() => { + if (timeUntilNextCheckIn > 0) { + setTimeUntilNextCheckIn(prev => Math.max(0, prev - 1)); + } + }, 1000); + + return () => clearInterval(interval); + }, [userAddress]); + + /** + * Load user's reward status + */ + const loadUserStatus = async () => { + try { + setIsLoadingStatus(true); + + const status = await getCheckInStatus(userAddress); + + setHasClaimedAirdrop(status.hasClaimedAirdrop); + setCanCheckIn(status.canCheckIn); + setTimeUntilNextCheckIn(status.timeUntilNext); + setCheckInStreak(status.streak); + setTotalRewards(status.totalRewards); + + } catch (error) { + console.error('Error loading status:', error); + Alert.alert('Error', 'Failed to load reward status'); + } finally { + setIsLoadingStatus(false); + } + }; + + /** + * Handle airdrop claim + */ + const handleClaimAirdrop = async () => { + Alert.alert( + 'Claim Airdrop', + 'This is a one-time reward. Are you sure you want to claim now?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Claim', + onPress: executeClaimAirdrop, + }, + ] + ); + }; + + const executeClaimAirdrop = async () => { + try { + setIsClaimingAirdrop(true); + + const result = await claimAirdrop(userAddress, nullifierHash); + + if (result.success) { + Alert.alert( + 'Airdrop Claimed! 🎉', + `You received ${result.amount} ZEA tokens!\nTransaction: ${result.txHash}`, + [ + { + text: 'OK', + onPress: loadUserStatus, + }, + ] + ); + } else { + throw new Error(result.error || 'Claim failed'); + } + } catch (error: any) { + console.error('Airdrop claim error:', error); + Alert.alert('Claim Failed', error.message || 'Failed to claim airdrop'); + } finally { + setIsClaimingAirdrop(false); + } + }; + + /** + * Handle daily check-in + */ + const handleCheckIn = async () => { + try { + setIsCheckingIn(true); + + const result = await dailyCheckIn(userAddress, nullifierHash); + + if (result.success) { + Alert.alert( + 'Check-in Successful! ✅', + `You earned ${result.amount} ZEA tokens!\nStreak: ${result.streak} days`, + [ + { + text: 'OK', + onPress: loadUserStatus, + }, + ] + ); + } else { + throw new Error(result.error || 'Check-in failed'); + } + } catch (error: any) { + console.error('Check-in error:', error); + Alert.alert('Check-in Failed', error.message || 'Failed to check in'); + } finally { + setIsCheckingIn(false); + } + }; + + /** + * Format time until next check-in + */ + const formatTimeRemaining = (seconds: number): string => { + if (seconds <= 0) return 'Available now!'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + return `${hours}h ${minutes}m ${secs}s`; + }; + + if (isLoadingStatus) { + return ( + + + Loading rewards... + + ); + } + + return ( + + {/* Header */} + + Rewards + + + + + + {/* Total Rewards Card */} + + Total Rewards Earned + {totalRewards} + ZEA Tokens + + + {/* Streak Card */} + + 🔥 + + Current Streak + {checkInStreak} days + + + + {/* Airdrop Section */} + {!hasClaimedAirdrop && ( + + + 🎁 + Welcome Airdrop + + + Claim your one-time welcome bonus! This reward is available to all verified users. + + + {isClaimingAirdrop ? ( + + ) : ( + Claim Airdrop + )} + + + )} + + {/* Daily Check-in Section */} + + + 📅 + Daily Check-in + + + Check in every day to earn rewards and build your streak! + + + {canCheckIn ? ( + + {isCheckingIn ? ( + + ) : ( + Check In Now ✨ + )} + + ) : ( + + Next check-in available in: + + {formatTimeRemaining(timeUntilNextCheckIn)} + + + )} + + + {/* Rewards Info */} + + How Rewards Work + + + + Check in daily to earn ZEA tokens + + + + + + Build streaks for bonus rewards (coming soon) + + + + + + All rewards are instantly sent to your wallet + + + + + + World ID verification required for all claims + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0F172A', + }, + loadingContainer: { + flex: 1, + backgroundColor: '#0F172A', + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + color: '#94A3B8', + marginTop: 16, + fontSize: 14, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 24, + paddingTop: 60, + }, + headerTitle: { + fontSize: 28, + fontWeight: 'bold', + color: '#FFFFFF', + }, + refreshButton: { + fontSize: 24, + color: '#6366F1', + }, + totalCard: { + backgroundColor: '#6366F1', + borderRadius: 16, + padding: 24, + marginHorizontal: 24, + marginBottom: 16, + alignItems: 'center', + }, + totalLabel: { + fontSize: 14, + color: '#E0E7FF', + marginBottom: 8, + }, + totalAmount: { + fontSize: 36, + fontWeight: 'bold', + color: '#FFFFFF', + marginBottom: 4, + }, + totalSymbol: { + fontSize: 14, + color: '#E0E7FF', + }, + streakCard: { + backgroundColor: '#1E293B', + borderRadius: 12, + padding: 16, + marginHorizontal: 24, + marginBottom: 24, + flexDirection: 'row', + alignItems: 'center', + }, + streakIcon: { + fontSize: 32, + marginRight: 16, + }, + streakInfo: { + flex: 1, + }, + streakLabel: { + fontSize: 14, + color: '#94A3B8', + marginBottom: 4, + }, + streakDays: { + fontSize: 20, + fontWeight: 'bold', + color: '#FFFFFF', + }, + airdropSection: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginHorizontal: 24, + marginBottom: 16, + }, + checkInSection: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginHorizontal: 24, + marginBottom: 24, + }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + sectionIcon: { + fontSize: 24, + marginRight: 8, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '600', + color: '#FFFFFF', + }, + sectionDescription: { + fontSize: 14, + color: '#94A3B8', + lineHeight: 20, + marginBottom: 16, + }, + claimButton: { + backgroundColor: '#10B981', + borderRadius: 12, + padding: 16, + alignItems: 'center', + }, + claimButtonDisabled: { + backgroundColor: '#4B5563', + }, + claimButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + checkInButton: { + backgroundColor: '#6366F1', + borderRadius: 12, + padding: 16, + alignItems: 'center', + }, + checkInButtonDisabled: { + backgroundColor: '#4B5563', + }, + checkInButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + cooldownCard: { + backgroundColor: '#0F172A', + borderRadius: 12, + padding: 16, + alignItems: 'center', + }, + cooldownLabel: { + fontSize: 14, + color: '#94A3B8', + marginBottom: 8, + }, + cooldownTime: { + fontSize: 18, + fontWeight: '600', + color: '#6366F1', + }, + infoSection: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginHorizontal: 24, + marginBottom: 32, + }, + infoTitle: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + marginBottom: 16, + }, + infoItem: { + flexDirection: 'row', + marginBottom: 12, + }, + infoBullet: { + color: '#6366F1', + marginRight: 8, + fontSize: 16, + }, + infoText: { + flex: 1, + fontSize: 13, + color: '#CBD5E1', + lineHeight: 18, + }, +}); diff --git a/mini-app/src/screens/SwapTradeScreen.tsx b/mini-app/src/screens/SwapTradeScreen.tsx new file mode 100644 index 0000000..45c5e5d --- /dev/null +++ b/mini-app/src/screens/SwapTradeScreen.tsx @@ -0,0 +1,592 @@ +/** + * SwapTradeScreen.tsx + * Token swap interface for DEX integration + * + * Features: + * - Token swap (e.g., WLD <-> ETH) + * - Price quotes from DEX + * - Slippage tolerance + * - Transaction preview + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + TextInput, + ActivityIndicator, + Alert, + ScrollView, +} from 'react'; +import { getSwapQuote, executeSwap } from '../services/swap'; + +interface SwapTradeScreenProps { + userAddress: string; +} + +const AVAILABLE_TOKENS = [ + { symbol: 'WLD', name: 'Worldcoin', icon: '🌐' }, + { symbol: 'ETH', name: 'Ethereum', icon: '⟠' }, + { symbol: 'ZEA', name: 'ZeaZDev Token', icon: '💎' }, + { symbol: 'USDC', name: 'USD Coin', icon: '💵' }, +]; + +export default function SwapTradeScreen({ userAddress }: SwapTradeScreenProps) { + const [fromToken, setFromToken] = useState(AVAILABLE_TOKENS[0]); + const [toToken, setToToken] = useState(AVAILABLE_TOKENS[1]); + const [fromAmount, setFromAmount] = useState(''); + const [toAmount, setToAmount] = useState(''); + const [slippage, setSlippage] = useState('0.5'); + + const [isLoadingQuote, setIsLoadingQuote] = useState(false); + const [isSwapping, setIsSwapping] = useState(false); + const [showTokenSelector, setShowTokenSelector] = useState<'from' | 'to' | null>(null); + + // Quote data + const [exchangeRate, setExchangeRate] = useState('0'); + const [priceImpact, setPriceImpact] = useState('0'); + const [estimatedGas, setEstimatedGas] = useState('0'); + + useEffect(() => { + if (fromAmount && parseFloat(fromAmount) > 0) { + loadQuote(); + } else { + setToAmount(''); + setExchangeRate('0'); + } + }, [fromAmount, fromToken, toToken]); + + /** + * Load swap quote from DEX + */ + const loadQuote = async () => { + try { + setIsLoadingQuote(true); + + const quote = await getSwapQuote({ + fromToken: fromToken.symbol, + toToken: toToken.symbol, + amount: fromAmount, + }); + + setToAmount(quote.outputAmount); + setExchangeRate(quote.exchangeRate); + setPriceImpact(quote.priceImpact); + setEstimatedGas(quote.estimatedGas); + + } catch (error) { + console.error('Error loading quote:', error); + setToAmount(''); + } finally { + setIsLoadingQuote(false); + } + }; + + /** + * Handle token swap + */ + const handleSwap = async () => { + // Validation + if (!fromAmount || parseFloat(fromAmount) <= 0) { + Alert.alert('Invalid Amount', 'Please enter an amount to swap'); + return; + } + + if (!toAmount || parseFloat(toAmount) <= 0) { + Alert.alert('Invalid Quote', 'Unable to get swap quote. Please try again.'); + return; + } + + const priceImpactNum = parseFloat(priceImpact); + if (priceImpactNum > 5) { + Alert.alert( + 'High Price Impact', + `Price impact is ${priceImpact}%. This trade may result in significant slippage.`, + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Continue Anyway', onPress: confirmSwap }, + ] + ); + return; + } + + confirmSwap(); + }; + + /** + * Confirm and execute swap + */ + const confirmSwap = async () => { + Alert.alert( + 'Confirm Swap', + `Swap ${fromAmount} ${fromToken.symbol} for ${toAmount} ${toToken.symbol}?\n\nEstimated Gas: ${estimatedGas} ETH`, + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Confirm', onPress: executeSwapTransaction }, + ] + ); + }; + + /** + * Execute the swap transaction + */ + const executeSwapTransaction = async () => { + try { + setIsSwapping(true); + + const result = await executeSwap({ + userAddress, + fromToken: fromToken.symbol, + toToken: toToken.symbol, + fromAmount, + toAmount, + slippage, + }); + + if (result.success) { + Alert.alert( + 'Swap Successful! 🎉', + `Transaction hash:\n${result.txHash}`, + [ + { + text: 'OK', + onPress: () => { + setFromAmount(''); + setToAmount(''); + }, + }, + ] + ); + } else { + throw new Error(result.error || 'Swap failed'); + } + } catch (error: any) { + console.error('Swap error:', error); + Alert.alert('Swap Failed', error.message || 'Failed to execute swap'); + } finally { + setIsSwapping(false); + } + }; + + /** + * Swap from and to tokens + */ + const swapTokenPositions = () => { + const temp = fromToken; + setFromToken(toToken); + setToToken(temp); + setFromAmount(toAmount); + setToAmount(''); + }; + + /** + * Select token from list + */ + const selectToken = (token: typeof AVAILABLE_TOKENS[0]) => { + if (showTokenSelector === 'from') { + if (token.symbol !== toToken.symbol) { + setFromToken(token); + } + } else if (showTokenSelector === 'to') { + if (token.symbol !== fromToken.symbol) { + setToToken(token); + } + } + setShowTokenSelector(null); + }; + + return ( + + {/* Header */} + + Swap + {}}> + ⚙️ + + + + {/* Swap Card */} + + {/* From Token */} + + From + + setShowTokenSelector('from')} + > + {fromToken.icon} + {fromToken.symbol} + + + + + + + {/* Swap Button */} + + + + + {/* To Token */} + + To (estimated) + + setShowTokenSelector('to')} + > + {toToken.icon} + {toToken.symbol} + + + + {isLoadingQuote ? ( + + ) : ( + + {toAmount || '0.0'} + + )} + + + + + {/* Exchange Rate */} + {exchangeRate !== '0' && ( + + Exchange Rate + + 1 {fromToken.symbol} = {exchangeRate} {toToken.symbol} + + + )} + + {/* Swap Details */} + {toAmount && ( + + + Price Impact + 3 && styles.detailValueWarning, + ]} + > + {priceImpact}% + + + + Slippage Tolerance + {slippage}% + + + Estimated Gas + {estimatedGas} ETH + + + )} + + {/* Swap Button */} + + {isSwapping ? ( + + ) : ( + + {!fromAmount ? 'Enter amount' : 'Swap'} + + )} + + + + {/* Token Selector Modal */} + {showTokenSelector && ( + + + + Select Token + setShowTokenSelector(null)}> + + + + {AVAILABLE_TOKENS.map((token) => ( + selectToken(token)} + > + {token.icon} + + {token.symbol} + {token.name} + + + ))} + + + )} + + {/* Info Section */} + + Swap Information + + • Powered by decentralized exchange (DEX) protocols + + + • No registration or KYC required + + + • You always remain in control of your funds + + + • Slippage tolerance can be adjusted in settings + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0F172A', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 24, + paddingTop: 60, + }, + headerTitle: { + fontSize: 28, + fontWeight: 'bold', + color: '#FFFFFF', + }, + settingsIcon: { + fontSize: 20, + }, + swapCard: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginHorizontal: 24, + marginBottom: 24, + }, + tokenSection: { + marginBottom: 12, + }, + sectionLabel: { + fontSize: 12, + color: '#94A3B8', + marginBottom: 8, + }, + tokenInputRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + tokenSelector: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#0F172A', + borderRadius: 12, + padding: 12, + minWidth: 120, + }, + tokenIcon: { + fontSize: 24, + marginRight: 8, + }, + tokenSymbol: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + flex: 1, + }, + dropdownIcon: { + fontSize: 10, + color: '#94A3B8', + }, + amountInput: { + flex: 1, + backgroundColor: '#0F172A', + borderRadius: 12, + padding: 12, + color: '#FFFFFF', + fontSize: 20, + textAlign: 'right', + }, + amountDisplay: { + flex: 1, + backgroundColor: '#0F172A', + borderRadius: 12, + padding: 12, + alignItems: 'flex-end', + justifyContent: 'center', + minHeight: 48, + }, + amountText: { + color: '#FFFFFF', + fontSize: 20, + }, + swapIconButton: { + alignSelf: 'center', + backgroundColor: '#6366F1', + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + marginVertical: 8, + }, + swapIconText: { + fontSize: 20, + color: '#FFFFFF', + }, + rateCard: { + backgroundColor: '#0F172A', + borderRadius: 8, + padding: 12, + marginTop: 12, + alignItems: 'center', + }, + rateLabel: { + fontSize: 12, + color: '#94A3B8', + marginBottom: 4, + }, + rateValue: { + fontSize: 14, + color: '#FFFFFF', + fontWeight: '500', + }, + detailsCard: { + marginTop: 12, + }, + detailRow: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 8, + }, + detailLabel: { + fontSize: 13, + color: '#94A3B8', + }, + detailValue: { + fontSize: 13, + color: '#FFFFFF', + fontWeight: '500', + }, + detailValueWarning: { + color: '#F59E0B', + }, + swapButton: { + backgroundColor: '#6366F1', + borderRadius: 12, + padding: 16, + alignItems: 'center', + marginTop: 16, + }, + swapButtonDisabled: { + backgroundColor: '#4B5563', + }, + swapButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + modalOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0.7)', + justifyContent: 'center', + padding: 24, + }, + modalContent: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + }, + modalTitle: { + fontSize: 18, + fontWeight: '600', + color: '#FFFFFF', + }, + modalClose: { + fontSize: 24, + color: '#94A3B8', + }, + tokenOption: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: '#0F172A', + borderRadius: 12, + marginBottom: 8, + }, + tokenOptionIcon: { + fontSize: 32, + marginRight: 12, + }, + tokenOptionInfo: { + flex: 1, + }, + tokenOptionSymbol: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + marginBottom: 2, + }, + tokenOptionName: { + fontSize: 12, + color: '#94A3B8', + }, + infoSection: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginHorizontal: 24, + marginBottom: 32, + }, + infoTitle: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + marginBottom: 12, + }, + infoText: { + fontSize: 13, + color: '#CBD5E1', + marginBottom: 8, + lineHeight: 18, + }, +}); diff --git a/mini-app/src/screens/WalletScreen.tsx b/mini-app/src/screens/WalletScreen.tsx new file mode 100644 index 0000000..77ee414 --- /dev/null +++ b/mini-app/src/screens/WalletScreen.tsx @@ -0,0 +1,496 @@ +/** + * WalletScreen.tsx + * Wallet management screen for ZeaZDev Mini App + * + * Features: + * - Display WLD and Gas token balances + * - Send tokens to other addresses + * - View transaction history + * - QR code for receiving + */ + +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + TextInput, + ScrollView, + ActivityIndicator, + Alert, +} from 'react-native'; +import { ethers } from 'ethers'; +import { getBalance, sendToken } from '../services/wallet'; + +interface WalletScreenProps { + userAddress: string; +} + +export default function WalletScreen({ userAddress }: WalletScreenProps) { + const [wldBalance, setWldBalance] = useState('0.00'); + const [gasBalance, setGasBalance] = useState('0.00'); + const [isLoading, setIsLoading] = useState(true); + const [isSending, setIsSending] = useState(false); + + // Send form state + const [showSendForm, setShowSendForm] = useState(false); + const [recipientAddress, setRecipientAddress] = useState(''); + const [sendAmount, setSendAmount] = useState(''); + const [selectedToken, setSelectedToken] = useState<'WLD' | 'GAS'>('WLD'); + + useEffect(() => { + loadBalances(); + + // Refresh balances every 10 seconds + const interval = setInterval(loadBalances, 10000); + return () => clearInterval(interval); + }, [userAddress]); + + /** + * Load wallet balances + */ + const loadBalances = async () => { + try { + setIsLoading(true); + + // Get WLD token balance + const wld = await getBalance(userAddress, 'WLD'); + setWldBalance(wld); + + // Get native gas token balance (ETH/MATIC) + const gas = await getBalance(userAddress, 'GAS'); + setGasBalance(gas); + + } catch (error) { + console.error('Error loading balances:', error); + Alert.alert('Error', 'Failed to load wallet balances'); + } finally { + setIsLoading(false); + } + }; + + /** + * Handle send token transaction + */ + const handleSend = async () => { + // Validation + if (!recipientAddress || !ethers.isAddress(recipientAddress)) { + Alert.alert('Invalid Address', 'Please enter a valid Ethereum address'); + return; + } + + if (!sendAmount || parseFloat(sendAmount) <= 0) { + Alert.alert('Invalid Amount', 'Please enter a valid amount'); + return; + } + + const balance = selectedToken === 'WLD' ? wldBalance : gasBalance; + if (parseFloat(sendAmount) > parseFloat(balance)) { + Alert.alert('Insufficient Balance', `You don't have enough ${selectedToken}`); + return; + } + + Alert.alert( + 'Confirm Transaction', + `Send ${sendAmount} ${selectedToken} to\n${recipientAddress.substring(0, 10)}...${recipientAddress.substring(recipientAddress.length - 8)}?`, + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Confirm', + onPress: executeSend, + }, + ] + ); + }; + + /** + * Execute send transaction + */ + const executeSend = async () => { + try { + setIsSending(true); + + const result = await sendToken({ + from: userAddress, + to: recipientAddress, + amount: sendAmount, + token: selectedToken, + }); + + if (result.success) { + Alert.alert( + 'Transaction Sent! 🎉', + `Transaction hash:\n${result.txHash}`, + [ + { + text: 'OK', + onPress: () => { + setShowSendForm(false); + setRecipientAddress(''); + setSendAmount(''); + loadBalances(); // Refresh balances + }, + }, + ] + ); + } else { + throw new Error(result.error || 'Transaction failed'); + } + } catch (error: any) { + console.error('Send error:', error); + Alert.alert('Transaction Failed', error.message || 'Failed to send tokens'); + } finally { + setIsSending(false); + } + }; + + /** + * Copy address to clipboard + */ + const copyAddress = () => { + // In React Native, you'd use Clipboard API + Alert.alert('Address Copied', userAddress); + }; + + return ( + + {/* Header */} + + My Wallet + + + {isLoading ? '⟳' : '↻'} + + + + + {/* Address Card */} + + Your Address + + {userAddress.substring(0, 16)}...{userAddress.substring(userAddress.length - 14)} + + Tap to copy + + + {/* Balance Cards */} + + {/* WLD Balance */} + + 🌐 + WLD Token + {isLoading ? ( + + ) : ( + {wldBalance} + )} + WLD + + + {/* Gas Balance */} + + + Gas Token + {isLoading ? ( + + ) : ( + {gasBalance} + )} + ETH + + + + {/* Action Buttons */} + + setShowSendForm(!showSendForm)} + > + 📤 + Send + + + {}}> + 📥 + Receive + + + {}}> + 📊 + History + + + + {/* Send Form */} + {showSendForm && ( + + Send Tokens + + {/* Token Selector */} + + setSelectedToken('WLD')} + > + WLD + + setSelectedToken('GAS')} + > + ETH + + + + {/* Recipient Address */} + + Recipient Address + + + + {/* Amount */} + + Amount + + + Balance: {selectedToken === 'WLD' ? wldBalance : gasBalance} {selectedToken} + + + + {/* Send Button */} + + {isSending ? ( + + ) : ( + Send {selectedToken} + )} + + + )} + + {/* Info Section */} + + Wallet Tips + + • Always verify recipient address before sending + + + • Keep some gas tokens for transaction fees + + + • Transactions are irreversible once confirmed + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0F172A', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 24, + paddingTop: 60, + }, + headerTitle: { + fontSize: 28, + fontWeight: 'bold', + color: '#FFFFFF', + }, + refreshButton: { + fontSize: 24, + color: '#6366F1', + }, + addressCard: { + backgroundColor: '#1E293B', + borderRadius: 12, + padding: 16, + marginHorizontal: 24, + marginBottom: 24, + }, + addressLabel: { + fontSize: 12, + color: '#94A3B8', + marginBottom: 8, + }, + addressText: { + fontSize: 14, + color: '#FFFFFF', + fontFamily: 'monospace', + marginBottom: 4, + }, + copyHint: { + fontSize: 10, + color: '#6366F1', + }, + balancesContainer: { + flexDirection: 'row', + paddingHorizontal: 24, + gap: 12, + marginBottom: 24, + }, + balanceCard: { + flex: 1, + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + alignItems: 'center', + }, + tokenIcon: { + fontSize: 32, + marginBottom: 8, + }, + balanceLabel: { + fontSize: 12, + color: '#94A3B8', + marginBottom: 12, + }, + balanceAmount: { + fontSize: 24, + fontWeight: 'bold', + color: '#FFFFFF', + marginBottom: 4, + }, + balanceSymbol: { + fontSize: 12, + color: '#6366F1', + }, + actionsContainer: { + flexDirection: 'row', + paddingHorizontal: 24, + gap: 12, + marginBottom: 24, + }, + actionButton: { + flex: 1, + backgroundColor: '#1E293B', + borderRadius: 12, + padding: 16, + alignItems: 'center', + }, + actionIcon: { + fontSize: 24, + marginBottom: 8, + }, + actionText: { + fontSize: 12, + color: '#FFFFFF', + }, + sendForm: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginHorizontal: 24, + marginBottom: 24, + }, + formTitle: { + fontSize: 18, + fontWeight: '600', + color: '#FFFFFF', + marginBottom: 20, + }, + tokenSelector: { + flexDirection: 'row', + gap: 12, + marginBottom: 20, + }, + tokenOption: { + flex: 1, + backgroundColor: '#0F172A', + borderRadius: 8, + padding: 12, + alignItems: 'center', + }, + tokenOptionActive: { + backgroundColor: '#6366F1', + }, + tokenOptionText: { + color: '#FFFFFF', + fontWeight: '600', + }, + inputGroup: { + marginBottom: 16, + }, + inputLabel: { + fontSize: 14, + color: '#94A3B8', + marginBottom: 8, + }, + input: { + backgroundColor: '#0F172A', + borderRadius: 8, + padding: 12, + color: '#FFFFFF', + fontSize: 14, + }, + balanceHint: { + fontSize: 12, + color: '#64748B', + marginTop: 4, + }, + sendButton: { + backgroundColor: '#6366F1', + borderRadius: 12, + padding: 16, + alignItems: 'center', + marginTop: 8, + }, + sendButtonDisabled: { + backgroundColor: '#4B5563', + }, + sendButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + infoSection: { + backgroundColor: '#1E293B', + borderRadius: 16, + padding: 20, + marginHorizontal: 24, + marginBottom: 32, + }, + infoTitle: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + marginBottom: 12, + }, + infoText: { + fontSize: 13, + color: '#CBD5E1', + marginBottom: 8, + lineHeight: 18, + }, +}); diff --git a/mini-app/src/services/rewards.ts b/mini-app/src/services/rewards.ts new file mode 100644 index 0000000..19ffcec --- /dev/null +++ b/mini-app/src/services/rewards.ts @@ -0,0 +1,129 @@ +/** + * rewards.ts + * Reward distribution service + */ + +import { API_URL } from '../config/constants'; + +interface CheckInStatusResponse { + canCheckIn: boolean; + timeUntilNext: number; + hasClaimedAirdrop: boolean; + streak: number; + totalRewards: string; +} + +/** + * Get user's check-in status + * @param address User's address + * @returns Check-in status + */ +export async function getCheckInStatus(address: string): Promise { + try { + const response = await fetch(`${API_URL}/api/checkin-status/${address}`); + + if (!response.ok) { + throw new Error('Failed to get status'); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Get check-in status error:', error); + // Return default values + return { + canCheckIn: true, + timeUntilNext: 0, + hasClaimedAirdrop: false, + streak: 0, + totalRewards: '0', + }; + } +} + +interface ClaimResult { + success: boolean; + amount?: string; + txHash?: string; + streak?: number; + error?: string; +} + +/** + * Claim initial airdrop + * @param address User's address + * @param nullifierHash User's World ID nullifier + * @returns Claim result + */ +export async function claimAirdrop( + address: string, + nullifierHash: string +): Promise { + try { + const response = await fetch(`${API_URL}/api/claim-airdrop`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ address, nullifierHash }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Claim failed'); + } + + const data = await response.json(); + return { + success: true, + amount: data.amount, + txHash: data.txHash, + }; + } catch (error: any) { + console.error('Claim airdrop error:', error); + return { + success: false, + error: error.message || 'Failed to claim airdrop', + }; + } +} + +/** + * Daily check-in to claim rewards + * @param address User's address + * @param nullifierHash User's World ID nullifier + * @returns Check-in result + */ +export async function dailyCheckIn( + address: string, + nullifierHash: string +): Promise { + try { + const response = await fetch(`${API_URL}/api/daily-checkin`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ address, nullifierHash }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Check-in failed'); + } + + const data = await response.json(); + return { + success: true, + amount: data.amount, + txHash: data.txHash, + streak: data.streak, + }; + } catch (error: any) { + console.error('Daily check-in error:', error); + return { + success: false, + error: error.message || 'Failed to check in', + }; + } +} diff --git a/mini-app/src/services/swap.ts b/mini-app/src/services/swap.ts new file mode 100644 index 0000000..50bf8fa --- /dev/null +++ b/mini-app/src/services/swap.ts @@ -0,0 +1,101 @@ +/** + * swap.ts + * Token swap service for DEX integration + */ + +import { API_URL } from '../config/constants'; + +interface SwapQuoteParams { + fromToken: string; + toToken: string; + amount: string; +} + +interface SwapQuoteResponse { + outputAmount: string; + exchangeRate: string; + priceImpact: string; + estimatedGas: string; +} + +/** + * Get swap quote from DEX + * @param params Quote parameters + * @returns Quote data + */ +export async function getSwapQuote(params: SwapQuoteParams): Promise { + try { + const response = await fetch(`${API_URL}/api/swap-quote`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error('Failed to get quote'); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Get swap quote error:', error); + // Return mock data for demonstration + return { + outputAmount: '0', + exchangeRate: '0', + priceImpact: '0', + estimatedGas: '0.001', + }; + } +} + +interface ExecuteSwapParams { + userAddress: string; + fromToken: string; + toToken: string; + fromAmount: string; + toAmount: string; + slippage: string; +} + +interface ExecuteSwapResponse { + success: boolean; + txHash?: string; + error?: string; +} + +/** + * Execute token swap + * @param params Swap parameters + * @returns Transaction result + */ +export async function executeSwap(params: ExecuteSwapParams): Promise { + try { + const response = await fetch(`${API_URL}/api/execute-swap`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Swap failed'); + } + + const data = await response.json(); + return { + success: true, + txHash: data.txHash, + }; + } catch (error: any) { + console.error('Execute swap error:', error); + return { + success: false, + error: error.message || 'Failed to execute swap', + }; + } +} diff --git a/mini-app/src/services/wallet.ts b/mini-app/src/services/wallet.ts new file mode 100644 index 0000000..21977cd --- /dev/null +++ b/mini-app/src/services/wallet.ts @@ -0,0 +1,90 @@ +/** + * wallet.ts + * Wallet service for managing tokens and transactions + */ + +import { ethers } from 'ethers'; +import { API_URL, RPC_URL } from '../config/constants'; + +/** + * Get token balance + * @param address User's address + * @param token Token symbol (WLD, GAS, etc.) + * @returns Balance as string + */ +export async function getBalance(address: string, token: 'WLD' | 'GAS'): Promise { + try { + // Mock implementation - replace with actual RPC calls + const provider = new ethers.JsonRpcProvider(RPC_URL); + + if (token === 'GAS') { + // Get native token balance (ETH/MATIC) + const balance = await provider.getBalance(address); + return ethers.formatEther(balance); + } else { + // Get ERC-20 token balance (WLD) + // This would require the token contract address and ABI + // Mock for demonstration + return '100.00'; + } + } catch (error) { + console.error('Get balance error:', error); + return '0.00'; + } +} + +interface SendTokenParams { + from: string; + to: string; + amount: string; + token: 'WLD' | 'GAS'; +} + +interface SendTokenResponse { + success: boolean; + txHash?: string; + error?: string; +} + +/** + * Send tokens to another address + * @param params Transaction parameters + * @returns Transaction result + */ +export async function sendToken(params: SendTokenParams): Promise { + try { + // This would normally: + // 1. Get user's private key from secure storage + // 2. Create and sign transaction + // 3. Broadcast to network + + // Mock implementation + console.log('Sending token:', params); + + // Simulate API call to relayer for gasless transaction + const response = await fetch(`${API_URL}/api/send-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error('Transaction failed'); + } + + const data = await response.json(); + + return { + success: true, + txHash: data.txHash || '0x1234...mock', + }; + } catch (error: any) { + console.error('Send token error:', error); + return { + success: false, + error: error.message || 'Failed to send token', + }; + } +} diff --git a/mini-app/src/services/worldid.ts b/mini-app/src/services/worldid.ts new file mode 100644 index 0000000..4472987 --- /dev/null +++ b/mini-app/src/services/worldid.ts @@ -0,0 +1,71 @@ +/** + * worldid.ts + * World ID verification service + * Handles Zero-Knowledge Proof verification + */ + +import { API_URL } from '../config/constants'; + +interface VerifyProofParams { + signal: string; // User's address + merkle_root: string; // Merkle root from World ID + nullifier_hash: string; // Unique identifier + proof: string; // ZK proof +} + +interface VerifyProofResponse { + success: boolean; + message?: string; + verified?: boolean; +} + +/** + * Verify World ID proof with backend + * @param params Proof data from World ID + * @returns Verification result + */ +export async function verifyProof(params: VerifyProofParams): Promise { + try { + const response = await fetch(`${API_URL}/api/verify-worldid`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error: any) { + console.error('World ID verification error:', error); + return { + success: false, + message: error.message || 'Failed to verify World ID', + }; + } +} + +/** + * Check if user is already verified + * @param address User's Ethereum address + * @returns Verification status + */ +export async function checkVerificationStatus(address: string): Promise { + try { + const response = await fetch(`${API_URL}/api/check-verification/${address}`); + + if (!response.ok) { + return false; + } + + const data = await response.json(); + return data.isVerified || false; + } catch (error) { + console.error('Check verification error:', error); + return false; + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..480f15e --- /dev/null +++ b/server/package.json @@ -0,0 +1,21 @@ +{ + "name": "zeazdev-verifier-server", + "version": "1.0.0", + "description": "Backend verifier service for ZeaZDev Mini App", + "main": "verifier.js", + "scripts": { + "start": "node verifier.js", + "dev": "nodemon verifier.js" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "ethers": "^6.10.0", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "nodemon": "^3.0.2" + }, + "author": "PHIPHAT PHOEMSUK (ZeaZDev)", + "license": "MIT" +} diff --git a/server/verifier.js b/server/verifier.js new file mode 100644 index 0000000..abaf48b --- /dev/null +++ b/server/verifier.js @@ -0,0 +1,344 @@ +/** + * verifier.js + * Backend server for World ID verification and reward distribution + * + * This server: + * 1. Receives Zero-Knowledge Proofs from frontend + * 2. Verifies proofs with World ID + * 3. Interacts with smart contracts for rewards + * 4. Acts as a relayer for gasless transactions + */ + +const express = require('express'); +const cors = require('cors'); +const { ethers } = require('ethers'); +require('dotenv').config(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Configuration +const WORLD_APP_ID = process.env.WORLD_APP_ID; +const WORLD_APP_API_KEY = process.env.WORLD_APP_API_KEY; +const RPC_URL = process.env.RPC_URL || 'https://worldchain-mainnet.g.alchemy.com/v2/your-key'; +const RELAYER_PRIVATE_KEY = process.env.RELAYER_PRIVATE_KEY; +const WORLD_ID_REWARDS_CONTRACT = process.env.WORLD_ID_REWARDS_CONTRACT; + +// Initialize provider and relayer wallet +const provider = new ethers.JsonRpcProvider(RPC_URL); +const relayerWallet = new ethers.Wallet(RELAYER_PRIVATE_KEY, provider); + +// WorldIDRewards Contract ABI (simplified) +const REWARDS_ABI = [ + 'function verifyAndRegister(address signal, uint256 root, uint256 nullifierHash, uint256[8] calldata proof) external', + 'function claimAirdrop() external', + 'function dailyCheckIn() external', + 'function getCheckInStatus(address user) external view returns (bool canCheckIn, uint256 timeUntilNext)', + 'function getUserData(address user) external view returns (bool isVerified, bool hasClaimedAirdrop, uint256 lastCheckIn, uint256 totalRewardsClaimed, uint256 checkInStreak)', +]; + +const rewardsContract = new ethers.Contract( + WORLD_ID_REWARDS_CONTRACT, + REWARDS_ABI, + relayerWallet +); + +/** + * POST /api/verify-worldid + * Verify World ID proof and register user + */ +app.post('/api/verify-worldid', async (req, res) => { + try { + const { signal, merkle_root, nullifier_hash, proof } = req.body; + + console.log('Verifying World ID proof...'); + console.log('Signal (address):', signal); + console.log('Nullifier Hash:', nullifier_hash); + + // Step 1: Verify with World ID API + const worldIdVerification = await verifyWithWorldID({ + merkle_root, + nullifier_hash, + proof, + }); + + if (!worldIdVerification.success) { + return res.status(400).json({ + success: false, + message: 'World ID verification failed', + }); + } + + // Step 2: Register on smart contract + // Convert proof to uint256[8] array format + const proofArray = parseProof(proof); + + const tx = await rewardsContract.verifyAndRegister( + signal, + merkle_root, + nullifier_hash, + proofArray + ); + + await tx.wait(); + + console.log('User registered successfully. TX:', tx.hash); + + res.json({ + success: true, + verified: true, + txHash: tx.hash, + message: 'World ID verified and user registered', + }); + } catch (error) { + console.error('Verification error:', error); + res.status(500).json({ + success: false, + message: error.message || 'Verification failed', + }); + } +}); + +/** + * GET /api/check-verification/:address + * Check if user is verified + */ +app.get('/api/check-verification/:address', async (req, res) => { + try { + const { address } = req.params; + + const userData = await rewardsContract.getUserData(address); + const [isVerified] = userData; + + res.json({ + isVerified, + }); + } catch (error) { + console.error('Check verification error:', error); + res.status(500).json({ + isVerified: false, + error: error.message, + }); + } +}); + +/** + * GET /api/checkin-status/:address + * Get user's check-in status + */ +app.get('/api/checkin-status/:address', async (req, res) => { + try { + const { address } = req.params; + + const [canCheckIn, timeUntilNext] = await rewardsContract.getCheckInStatus(address); + const userData = await rewardsContract.getUserData(address); + const [, hasClaimedAirdrop, , totalRewardsClaimed, checkInStreak] = userData; + + res.json({ + canCheckIn, + timeUntilNext: Number(timeUntilNext), + hasClaimedAirdrop, + streak: Number(checkInStreak), + totalRewards: ethers.formatEther(totalRewardsClaimed), + }); + } catch (error) { + console.error('Get check-in status error:', error); + res.status(500).json({ + error: error.message, + }); + } +}); + +/** + * POST /api/claim-airdrop + * Claim initial airdrop + */ +app.post('/api/claim-airdrop', async (req, res) => { + try { + const { address, nullifierHash } = req.body; + + console.log('Processing airdrop claim for:', address); + + // Execute transaction via relayer (gasless for user) + const tx = await rewardsContract.claimAirdrop(); + const receipt = await tx.wait(); + + console.log('Airdrop claimed. TX:', tx.hash); + + res.json({ + success: true, + txHash: tx.hash, + amount: '1000', // Update with actual amount from event + }); + } catch (error) { + console.error('Claim airdrop error:', error); + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +/** + * POST /api/daily-checkin + * Daily check-in for rewards + */ +app.post('/api/daily-checkin', async (req, res) => { + try { + const { address, nullifierHash } = req.body; + + console.log('Processing daily check-in for:', address); + + // Execute transaction via relayer + const tx = await rewardsContract.dailyCheckIn(); + const receipt = await tx.wait(); + + // Parse event to get streak + // In production, parse the DailyCheckIn event + const streak = 1; // Mock value + + console.log('Check-in successful. TX:', tx.hash); + + res.json({ + success: true, + txHash: tx.hash, + amount: '100', // Update with actual amount + streak, + }); + } catch (error) { + console.error('Daily check-in error:', error); + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +/** + * POST /api/swap-quote + * Get swap quote from DEX + */ +app.post('/api/swap-quote', async (req, res) => { + try { + const { fromToken, toToken, amount } = req.body; + + // Mock implementation - integrate with actual DEX router + // e.g., Uniswap V3, Sushiswap + + const exchangeRate = '1.05'; // Mock rate + const outputAmount = (parseFloat(amount) * parseFloat(exchangeRate)).toFixed(4); + const priceImpact = '0.5'; + + res.json({ + outputAmount, + exchangeRate, + priceImpact, + estimatedGas: '0.002', + }); + } catch (error) { + console.error('Get swap quote error:', error); + res.status(500).json({ + error: error.message, + }); + } +}); + +/** + * POST /api/execute-swap + * Execute token swap + */ +app.post('/api/execute-swap', async (req, res) => { + try { + const { userAddress, fromToken, toToken, fromAmount, toAmount, slippage } = req.body; + + console.log('Executing swap:', fromToken, '->', toToken); + + // Mock implementation - integrate with DEX router + // This would: + // 1. Approve tokens if needed + // 2. Call router's swap function + // 3. Return transaction hash + + res.json({ + success: true, + txHash: '0x' + '1234567890abcdef'.repeat(4), // Mock tx hash + }); + } catch (error) { + console.error('Execute swap error:', error); + res.status(500).json({ + success: false, + error: error.message, + }); + } +}); + +/** + * Helper: Verify with World ID API + */ +async function verifyWithWorldID({ merkle_root, nullifier_hash, proof }) { + try { + // Call World ID verification API + // https://developer.worldcoin.org/api/v1/verify + + const response = await fetch('https://developer.worldcoin.org/api/v1/verify', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${WORLD_APP_API_KEY}`, + }, + body: JSON.stringify({ + merkle_root, + nullifier_hash, + proof, + verification_level: 'orb', // or 'device' + action: process.env.WORLD_ACTION_ID, + }), + }); + + if (!response.ok) { + throw new Error('World ID API verification failed'); + } + + const data = await response.json(); + return { + success: data.success || false, + verified: data.verified || false, + }; + } catch (error) { + console.error('World ID API error:', error); + return { success: false }; + } +} + +/** + * Helper: Parse proof string to uint256[8] array + */ +function parseProof(proof) { + // Parse proof format from World ID + // This depends on the exact format returned by IDKit + // Mock implementation + return Array(8).fill('0'); +} + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`=================================`); + console.log(`ZeaZDev Verifier Server`); + console.log(`=================================`); + console.log(`Server running on port ${PORT}`); + console.log(`World App ID: ${WORLD_APP_ID}`); + console.log(`Contract: ${WORLD_ID_REWARDS_CONTRACT}`); + console.log(`Relayer: ${relayerWallet.address}`); + console.log(`=================================`); +}); + +module.exports = app;