feat(core): Add backend services, scripts, and initial test structure
@@ -0,0 +1,9 @@
|
||||
# PezkuwiChain WebSocket Endpoint
|
||||
WS_ENDPOINT=wss://ws.pezkuwichain.io
|
||||
|
||||
# Sudo account seed phrase for auto-approval
|
||||
# This account will sign approve_kyc transactions when threshold is reached
|
||||
SUDO_SEED=your_seed_phrase_here
|
||||
|
||||
# Server port
|
||||
PORT=3001
|
||||
@@ -0,0 +1,350 @@
|
||||
# 🏛️ KYC Council Backend
|
||||
|
||||
Backend simulation of pallet-collective voting for KYC approvals.
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
**Purpose:** Decentralized KYC approval system without runtime changes
|
||||
|
||||
**Architecture:**
|
||||
- Backend tracks council votes (in-memory)
|
||||
- 60% threshold (e.g., 7/11 votes)
|
||||
- Auto-executes approve_kyc when threshold reached
|
||||
- Uses SUDO account to sign blockchain transactions
|
||||
|
||||
## 🔗 Chain Flow
|
||||
|
||||
```
|
||||
User applies → Blockchain (PENDING) → Council votes → Backend auto-approves → Welati NFT minted
|
||||
```
|
||||
|
||||
**Why SUDO account?**
|
||||
- `identityKyc.approveKyc()` requires `EnsureRoot` origin
|
||||
- Backend signs transactions on behalf of council
|
||||
- Alternative: Change runtime to accept council origin (not needed for MVP)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
cd /home/mamostehp/pwap/backend
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Required variables:**
|
||||
```env
|
||||
WS_ENDPOINT=wss://ws.pezkuwichain.io
|
||||
SUDO_SEED=your_sudo_seed_phrase_here
|
||||
PORT=3001
|
||||
```
|
||||
|
||||
⚠️ **Security Warning:** Keep SUDO_SEED secret! Use a dedicated account for KYC approvals only.
|
||||
|
||||
### 3. Start Server
|
||||
|
||||
**Development (with hot reload):**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Production:**
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 API Endpoints
|
||||
|
||||
### Council Management
|
||||
|
||||
#### Add Council Member
|
||||
```bash
|
||||
POST /api/council/add-member
|
||||
{
|
||||
"address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
"signature": "0x..." // TODO: Implement signature verification
|
||||
}
|
||||
```
|
||||
|
||||
#### Remove Council Member
|
||||
```bash
|
||||
POST /api/council/remove-member
|
||||
{
|
||||
"address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Council Members
|
||||
```bash
|
||||
GET /api/council/members
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"members": ["5DFw...Dwd3", "5Grw...utQY"],
|
||||
"totalMembers": 2,
|
||||
"threshold": 0.6,
|
||||
"votesRequired": 2
|
||||
}
|
||||
```
|
||||
|
||||
#### Sync with Noter Tiki Holders
|
||||
```bash
|
||||
POST /api/council/sync-notaries
|
||||
```
|
||||
|
||||
Auto-fetches first 10 Noter tiki holders from blockchain and updates council.
|
||||
|
||||
---
|
||||
|
||||
### KYC Voting
|
||||
|
||||
#### Propose KYC Approval
|
||||
```bash
|
||||
POST /api/kyc/propose
|
||||
{
|
||||
"userAddress": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
"proposerAddress": "5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3",
|
||||
"signature": "0x..."
|
||||
}
|
||||
```
|
||||
|
||||
**Logic:**
|
||||
- Proposer auto-votes AYE
|
||||
- If threshold already met (e.g., 1/1 member), auto-executes immediately
|
||||
|
||||
#### Vote on Proposal
|
||||
```bash
|
||||
POST /api/kyc/vote
|
||||
{
|
||||
"userAddress": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
"voterAddress": "5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3",
|
||||
"approve": true,
|
||||
"signature": "0x..."
|
||||
}
|
||||
```
|
||||
|
||||
**Approve:** true = AYE, false = NAY
|
||||
|
||||
**Auto-execute:** If votes reach threshold, backend signs and submits `approve_kyc` transaction.
|
||||
|
||||
#### Get Pending Proposals
|
||||
```bash
|
||||
GET /api/kyc/pending
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"pending": [
|
||||
{
|
||||
"userAddress": "5Grw...utQY",
|
||||
"proposer": "5DFw...Dwd3",
|
||||
"ayes": ["5DFw...Dwd3", "5HpG...vSKr"],
|
||||
"nays": [],
|
||||
"timestamp": 1700000000000,
|
||||
"votesCount": 2,
|
||||
"threshold": 7,
|
||||
"status": "VOTING"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Council Membership Rules
|
||||
|
||||
### Initial Setup
|
||||
- **1 member:** Founder delegate (hardcoded: `5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3`)
|
||||
- **Threshold:** 1/1 = 100% (single vote approves)
|
||||
|
||||
### Growth Path
|
||||
1. **Add Noter holders:** Use `/api/council/sync-notaries` to fetch first 10 Noter tiki holders
|
||||
2. **Council size:** 11 members (1 founder + 10 notaries)
|
||||
3. **Threshold:** 7/11 = 63.6% (60% threshold met)
|
||||
|
||||
### Automatic Updates
|
||||
- When Noter loses tiki → Remove from council
|
||||
- When new Noter available → Add to council (first 10 priority)
|
||||
- If no Notaries available → Serok (president) can appoint manually
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Test 1: Single Member (Founder Only)
|
||||
```bash
|
||||
# 1. Start backend
|
||||
npm run dev
|
||||
|
||||
# 2. Get council members
|
||||
curl http://localhost:3001/api/council/members
|
||||
|
||||
# Expected: 1 member (founder delegate)
|
||||
|
||||
# 3. User applies KYC (via frontend)
|
||||
# 4. Propose approval
|
||||
curl -X POST http://localhost:3001/api/kyc/propose \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"userAddress": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
"proposerAddress": "5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3"
|
||||
}'
|
||||
|
||||
# Expected: Auto-execute (1/1 threshold reached)
|
||||
```
|
||||
|
||||
### Test 2: 3 Members
|
||||
```bash
|
||||
# 1. Add 2 notaries
|
||||
curl -X POST http://localhost:3001/api/council/add-member \
|
||||
-d '{"address": "5HpG...vSKr"}'
|
||||
|
||||
curl -X POST http://localhost:3001/api/council/add-member \
|
||||
-d '{"address": "5FLe...dXRp"}'
|
||||
|
||||
# 2. Council: 3 members, threshold = 2 votes (60% of 3 = 1.8 → ceil = 2)
|
||||
|
||||
# 3. Propose
|
||||
curl -X POST http://localhost:3001/api/kyc/propose \
|
||||
-d '{
|
||||
"userAddress": "5Grw...utQY",
|
||||
"proposerAddress": "5DFw...Dwd3"
|
||||
}'
|
||||
|
||||
# Status: 1/2 (proposer voted AYE)
|
||||
|
||||
# 4. Vote from member 2
|
||||
curl -X POST http://localhost:3001/api/kyc/vote \
|
||||
-d '{
|
||||
"userAddress": "5Grw...utQY",
|
||||
"voterAddress": "5HpG...vSKr",
|
||||
"approve": true
|
||||
}'
|
||||
|
||||
# Expected: Auto-execute (2/2 threshold reached) ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
GET /health
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"blockchain": "connected",
|
||||
"sudoAccount": "5DFw...Dwd3",
|
||||
"councilMembers": 11,
|
||||
"pendingVotes": 3
|
||||
}
|
||||
```
|
||||
|
||||
### Console Logs
|
||||
```
|
||||
🔗 Connecting to PezkuwiChain...
|
||||
✅ Sudo account loaded: 5DFw...Dwd3
|
||||
✅ Connected to blockchain
|
||||
📊 Chain: PezkuwiChain
|
||||
🏛️ Runtime version: 106
|
||||
|
||||
🚀 KYC Council Backend running on port 3001
|
||||
📊 Council members: 1
|
||||
🎯 Threshold: 60%
|
||||
|
||||
📝 KYC proposal created for 5Grw...utQY by 5DFw...Dwd3
|
||||
📊 Votes: 1/1 (1 members, 60% threshold)
|
||||
🎉 Threshold reached for 5Grw...utQY! Executing approve_kyc...
|
||||
📡 Transaction status: Ready
|
||||
📡 Transaction status: InBlock
|
||||
✅ KYC APPROVED for 5Grw...utQY
|
||||
🏛️ User will receive Welati NFT automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Integration with Frontend
|
||||
|
||||
### Option A: Direct Backend API Calls
|
||||
Frontend calls backend endpoints directly (simpler for MVP).
|
||||
|
||||
```typescript
|
||||
// Propose KYC approval
|
||||
const response = await fetch('http://localhost:3001/api/kyc/propose', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userAddress: application.address,
|
||||
proposerAddress: selectedAccount.address,
|
||||
signature: await signMessage(...)
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Option B: Blockchain Events + Backend Sync
|
||||
Backend listens to blockchain events and auto-tracks proposals (future enhancement).
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Security Considerations
|
||||
|
||||
1. **SUDO Account Protection:**
|
||||
- Use dedicated hardware wallet (Ledger recommended)
|
||||
- Only use for KYC approvals, nothing else
|
||||
- Consider multi-sig in production
|
||||
|
||||
2. **Signature Verification:**
|
||||
- TODO: Implement Polkadot signature verification
|
||||
- Prevent vote spam from non-members
|
||||
|
||||
3. **Rate Limiting:**
|
||||
- Add rate limiting to API endpoints
|
||||
- Prevent DoS attacks
|
||||
|
||||
4. **Audit Trail:**
|
||||
- Log all votes to database (future enhancement)
|
||||
- Track council member changes
|
||||
|
||||
---
|
||||
|
||||
## 📝 TODO
|
||||
|
||||
- [ ] Implement signature verification
|
||||
- [ ] Add database persistence (replace in-memory Maps)
|
||||
- [ ] Add rate limiting middleware
|
||||
- [ ] Add automated council sync cron job
|
||||
- [ ] Add multi-sig support for sudo account
|
||||
- [ ] Add audit logging
|
||||
- [ ] Add Prometheus metrics
|
||||
- [ ] Add Docker support
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Backend Developer:** [Your contact]
|
||||
**Runtime Issues:** Check `/Pezkuwi-SDK/pezkuwi/pallets/identity-kyc`
|
||||
**Frontend Integration:** See `/pwap/ADMIN_KYC_GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0 (Backend Council MVP)
|
||||
**Last Updated:** 17 Kasım 2025
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "pezkuwi-kyc-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "KYC Approval Council Backend",
|
||||
"main": "src/server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node --watch src/server.js",
|
||||
"start": "node src/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"@polkadot/api": "^10.11.1",
|
||||
"@polkadot/keyring": "^12.5.1",
|
||||
"@polkadot/util-crypto": "^12.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
|
||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// ========================================
|
||||
// KYC COUNCIL STATE
|
||||
// ========================================
|
||||
|
||||
// Council members (wallet addresses)
|
||||
const councilMembers = new Set([
|
||||
'5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3' // Initial: Founder's delegate
|
||||
]);
|
||||
|
||||
// Pending KYC votes: Map<userAddress, { ayes: Set, nays: Set, proposer, timestamp }>
|
||||
const kycVotes = new Map();
|
||||
|
||||
// Threshold: 60%
|
||||
const THRESHOLD_PERCENT = 0.6;
|
||||
|
||||
// Sudo account for signing approve_kyc
|
||||
let sudoAccount = null;
|
||||
let api = null;
|
||||
|
||||
// ========================================
|
||||
// BLOCKCHAIN CONNECTION
|
||||
// ========================================
|
||||
|
||||
async function initBlockchain() {
|
||||
console.log('🔗 Connecting to PezkuwiChain...');
|
||||
|
||||
const wsProvider = new WsProvider(process.env.WS_ENDPOINT || 'wss://ws.pezkuwichain.io');
|
||||
api = await ApiPromise.create({ provider: wsProvider });
|
||||
|
||||
await cryptoWaitReady();
|
||||
|
||||
// Initialize sudo account from env
|
||||
if (process.env.SUDO_SEED) {
|
||||
const keyring = new Keyring({ type: 'sr25519' });
|
||||
sudoAccount = keyring.addFromUri(process.env.SUDO_SEED);
|
||||
console.log('✅ Sudo account loaded:', sudoAccount.address);
|
||||
} else {
|
||||
console.warn('⚠️ No SUDO_SEED in .env - auto-approval disabled');
|
||||
}
|
||||
|
||||
console.log('✅ Connected to blockchain');
|
||||
console.log('📊 Chain:', await api.rpc.system.chain());
|
||||
console.log('🏛️ Runtime version:', api.runtimeVersion.specVersion.toNumber());
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COUNCIL MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
// Add member to council (only founder/sudo can call)
|
||||
app.post('/api/council/add-member', async (req, res) => {
|
||||
const { address, signature } = req.body;
|
||||
|
||||
// TODO: Verify signature from founder
|
||||
// For now, just add
|
||||
|
||||
if (!address || address.length < 47) {
|
||||
return res.status(400).json({ error: 'Invalid address' });
|
||||
}
|
||||
|
||||
councilMembers.add(address);
|
||||
|
||||
console.log(`✅ Council member added: ${address}`);
|
||||
console.log(`📊 Total members: ${councilMembers.size}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
totalMembers: councilMembers.size,
|
||||
members: Array.from(councilMembers)
|
||||
});
|
||||
});
|
||||
|
||||
// Remove member from council
|
||||
app.post('/api/council/remove-member', async (req, res) => {
|
||||
const { address } = req.body;
|
||||
|
||||
if (!councilMembers.has(address)) {
|
||||
return res.status(404).json({ error: 'Member not found' });
|
||||
}
|
||||
|
||||
councilMembers.delete(address);
|
||||
|
||||
console.log(`❌ Council member removed: ${address}`);
|
||||
console.log(`📊 Total members: ${councilMembers.size}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
totalMembers: councilMembers.size,
|
||||
members: Array.from(councilMembers)
|
||||
});
|
||||
});
|
||||
|
||||
// Get council members
|
||||
app.get('/api/council/members', (req, res) => {
|
||||
res.json({
|
||||
members: Array.from(councilMembers),
|
||||
totalMembers: councilMembers.size,
|
||||
threshold: THRESHOLD_PERCENT,
|
||||
votesRequired: Math.ceil(councilMembers.size * THRESHOLD_PERCENT)
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// KYC VOTING
|
||||
// ========================================
|
||||
|
||||
// Propose KYC approval
|
||||
app.post('/api/kyc/propose', async (req, res) => {
|
||||
const { userAddress, proposerAddress, signature } = req.body;
|
||||
|
||||
// Verify proposer is council member
|
||||
if (!councilMembers.has(proposerAddress)) {
|
||||
return res.status(403).json({ error: 'Not a council member' });
|
||||
}
|
||||
|
||||
// TODO: Verify signature
|
||||
|
||||
// Check if already has votes
|
||||
if (kycVotes.has(userAddress)) {
|
||||
return res.status(400).json({ error: 'Proposal already exists' });
|
||||
}
|
||||
|
||||
// Create vote record
|
||||
kycVotes.set(userAddress, {
|
||||
ayes: new Set([proposerAddress]), // Proposer auto-votes aye
|
||||
nays: new Set(),
|
||||
proposer: proposerAddress,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
console.log(`📝 KYC proposal created for ${userAddress} by ${proposerAddress}`);
|
||||
|
||||
// Check if threshold already met (e.g., only 1 member)
|
||||
await checkAndExecute(userAddress);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
userAddress,
|
||||
votesCount: 1,
|
||||
threshold: Math.ceil(councilMembers.size * THRESHOLD_PERCENT)
|
||||
});
|
||||
});
|
||||
|
||||
// Vote on KYC proposal
|
||||
app.post('/api/kyc/vote', async (req, res) => {
|
||||
const { userAddress, voterAddress, approve, signature } = req.body;
|
||||
|
||||
// Verify voter is council member
|
||||
if (!councilMembers.has(voterAddress)) {
|
||||
return res.status(403).json({ error: 'Not a council member' });
|
||||
}
|
||||
|
||||
// Check if proposal exists
|
||||
if (!kycVotes.has(userAddress)) {
|
||||
return res.status(404).json({ error: 'Proposal not found' });
|
||||
}
|
||||
|
||||
// TODO: Verify signature
|
||||
|
||||
const votes = kycVotes.get(userAddress);
|
||||
|
||||
// Add vote
|
||||
if (approve) {
|
||||
votes.nays.delete(voterAddress); // Remove from nays if exists
|
||||
votes.ayes.add(voterAddress);
|
||||
console.log(`✅ AYE vote from ${voterAddress} for ${userAddress}`);
|
||||
} else {
|
||||
votes.ayes.delete(voterAddress); // Remove from ayes if exists
|
||||
votes.nays.add(voterAddress);
|
||||
console.log(`❌ NAY vote from ${voterAddress} for ${userAddress}`);
|
||||
}
|
||||
|
||||
// Check if threshold reached
|
||||
await checkAndExecute(userAddress);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
ayes: votes.ayes.size,
|
||||
nays: votes.nays.size,
|
||||
threshold: Math.ceil(councilMembers.size * THRESHOLD_PERCENT),
|
||||
status: votes.ayes.size >= Math.ceil(councilMembers.size * THRESHOLD_PERCENT) ? 'APPROVED' : 'VOTING'
|
||||
});
|
||||
});
|
||||
|
||||
// Check if threshold reached and execute approve_kyc
|
||||
async function checkAndExecute(userAddress) {
|
||||
const votes = kycVotes.get(userAddress);
|
||||
if (!votes) return;
|
||||
|
||||
const requiredVotes = Math.ceil(councilMembers.size * THRESHOLD_PERCENT);
|
||||
const currentAyes = votes.ayes.size;
|
||||
|
||||
console.log(`📊 Votes: ${currentAyes}/${requiredVotes} (${councilMembers.size} members, ${THRESHOLD_PERCENT * 100}% threshold)`);
|
||||
|
||||
if (currentAyes >= requiredVotes) {
|
||||
console.log(`🎉 Threshold reached for ${userAddress}! Executing approve_kyc...`);
|
||||
|
||||
if (!sudoAccount || !api) {
|
||||
console.error('❌ Cannot execute: No sudo account or API connection');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Submit approve_kyc transaction
|
||||
const tx = api.tx.identityKyc.approveKyc(userAddress);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
tx.signAndSend(sudoAccount, ({ status, dispatchError, events }) => {
|
||||
console.log(`📡 Transaction status: ${status.type}`);
|
||||
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Transaction failed';
|
||||
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else {
|
||||
errorMessage = dispatchError.toString();
|
||||
}
|
||||
|
||||
console.error(`❌ Approval failed: ${errorMessage}`);
|
||||
reject(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for KycApproved event
|
||||
const approvedEvent = events.find(({ event }) =>
|
||||
event.section === 'identityKyc' && event.method === 'KycApproved'
|
||||
);
|
||||
|
||||
if (approvedEvent) {
|
||||
console.log(`✅ KYC APPROVED for ${userAddress}`);
|
||||
console.log(`🏛️ User will receive Welati NFT automatically`);
|
||||
|
||||
// Remove from pending votes
|
||||
kycVotes.delete(userAddress);
|
||||
|
||||
resolve();
|
||||
} else {
|
||||
console.warn('⚠️ Transaction included but no KycApproved event');
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}).catch(reject);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error executing approve_kyc:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get pending KYC votes
|
||||
app.get('/api/kyc/pending', (req, res) => {
|
||||
const pending = [];
|
||||
|
||||
for (const [userAddress, votes] of kycVotes.entries()) {
|
||||
pending.push({
|
||||
userAddress,
|
||||
proposer: votes.proposer,
|
||||
ayes: Array.from(votes.ayes),
|
||||
nays: Array.from(votes.nays),
|
||||
timestamp: votes.timestamp,
|
||||
votesCount: votes.ayes.size,
|
||||
threshold: Math.ceil(councilMembers.size * THRESHOLD_PERCENT),
|
||||
status: votes.ayes.size >= Math.ceil(councilMembers.size * THRESHOLD_PERCENT) ? 'APPROVED' : 'VOTING'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ pending });
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// AUTO-UPDATE COUNCIL FROM BLOCKCHAIN
|
||||
// ========================================
|
||||
|
||||
// Sync council with Noter tiki holders
|
||||
app.post('/api/council/sync-notaries', async (req, res) => {
|
||||
if (!api) {
|
||||
return res.status(503).json({ error: 'Blockchain not connected' });
|
||||
}
|
||||
|
||||
console.log('🔄 Syncing council with Noter tiki holders...');
|
||||
|
||||
try {
|
||||
// Get all users with tikis
|
||||
const entries = await api.query.tiki.userTikis.entries();
|
||||
|
||||
const notaries = [];
|
||||
const NOTER_INDEX = 9; // Noter tiki index
|
||||
|
||||
for (const [key, tikis] of entries) {
|
||||
const address = key.args[0].toString();
|
||||
const tikiList = tikis.toJSON();
|
||||
|
||||
// Check if user has Noter tiki
|
||||
if (tikiList && tikiList.includes(NOTER_INDEX)) {
|
||||
notaries.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📊 Found ${notaries.length} Noter tiki holders`);
|
||||
|
||||
// Add first 10 notaries to council
|
||||
const founderDelegate = '5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3';
|
||||
councilMembers.clear();
|
||||
councilMembers.add(founderDelegate);
|
||||
|
||||
notaries.slice(0, 10).forEach(address => {
|
||||
councilMembers.add(address);
|
||||
});
|
||||
|
||||
console.log(`✅ Council updated: ${councilMembers.size} members`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
totalMembers: councilMembers.size,
|
||||
members: Array.from(councilMembers),
|
||||
notariesFound: notaries.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error syncing notaries:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// HEALTH CHECK
|
||||
// ========================================
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
blockchain: api ? 'connected' : 'disconnected',
|
||||
sudoAccount: sudoAccount ? sudoAccount.address : 'not configured',
|
||||
councilMembers: councilMembers.size,
|
||||
pendingVotes: kycVotes.size
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// START SERVER
|
||||
// ========================================
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
initBlockchain()
|
||||
.then(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 KYC Council Backend running on port ${PORT}`);
|
||||
console.log(`📊 Council members: ${councilMembers.size}`);
|
||||
console.log(`🎯 Threshold: ${THRESHOLD_PERCENT * 100}%`);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Failed to initialize blockchain:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Creates NFT Collection #42 for Tiki citizenship system
|
||||
*
|
||||
* IMPORTANT: This script ensures Collection 42 exists before citizenship NFTs can be minted.
|
||||
* Must be run after chain initialization and before any citizenship operations.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/create_collection_42.js [ws://127.0.0.1:9944]
|
||||
*/
|
||||
|
||||
const { ApiPromise, WsProvider, Keyring } = require('@polkadot/api');
|
||||
|
||||
async function createCollection42() {
|
||||
// Get WebSocket endpoint from args or use default
|
||||
const wsEndpoint = process.argv[2] || 'ws://127.0.0.1:9944';
|
||||
|
||||
const wsProvider = new WsProvider(wsEndpoint);
|
||||
const api = await ApiPromise.create({ provider: wsProvider });
|
||||
|
||||
const keyring = new Keyring({ type: 'sr25519' });
|
||||
const alice = keyring.addFromUri('//Alice');
|
||||
|
||||
console.log(`🔗 Connected to ${wsEndpoint}\n`);
|
||||
console.log('🎯 Target: Create NFT Collection #42 for Tiki citizenship system\n');
|
||||
|
||||
// Check current NextCollectionId
|
||||
const nextCollectionId = await api.query.nfts.nextCollectionId();
|
||||
const currentId = nextCollectionId.isNone ? 0 : nextCollectionId.unwrap().toNumber();
|
||||
|
||||
console.log(`📊 Current NextCollectionId: ${currentId}`);
|
||||
|
||||
if (currentId > 42) {
|
||||
console.log('❌ ERROR: NextCollectionId is already past 42!');
|
||||
console.log(' Collection 42 cannot be created anymore.');
|
||||
console.log(' You need to start with a fresh chain.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (currentId === 42) {
|
||||
console.log('✅ NextCollectionId is exactly 42! Creating Collection 42 now...\n');
|
||||
await createSingleCollection(api, alice, 42);
|
||||
await api.disconnect();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Need to create multiple collections to reach 42
|
||||
const collectionsToCreate = 42 - currentId + 1;
|
||||
console.log(`📝 Need to create ${collectionsToCreate} collections (IDs ${currentId} through 42)\n`);
|
||||
|
||||
// Create collections in batches to reach 42
|
||||
for (let i = currentId; i <= 42; i++) {
|
||||
await createSingleCollection(api, alice, i);
|
||||
}
|
||||
|
||||
console.log('\n🎉 Success! Collection 42 has been created and is ready for Tiki citizenship NFTs.');
|
||||
console.log(' You can now use the self-confirmation citizenship system.');
|
||||
|
||||
await api.disconnect();
|
||||
}
|
||||
|
||||
async function createSingleCollection(api, signer, expectedId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = api.createType('PalletNftsCollectionConfig', {
|
||||
settings: 0,
|
||||
maxSupply: null,
|
||||
mintSettings: {
|
||||
mintType: { Issuer: null },
|
||||
price: null,
|
||||
startBlock: null,
|
||||
endBlock: null,
|
||||
defaultItemSettings: 0
|
||||
}
|
||||
});
|
||||
|
||||
const tx = api.tx.sudo.sudo(
|
||||
api.tx.nfts.forceCreate(
|
||||
{ Id: signer.address },
|
||||
config
|
||||
)
|
||||
);
|
||||
|
||||
console.log(` Creating Collection #${expectedId}...`);
|
||||
|
||||
tx.signAndSend(signer, ({ status, events, dispatchError }) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Transaction failed';
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else {
|
||||
errorMessage = dispatchError.toString();
|
||||
}
|
||||
console.log(` ❌ Error: ${errorMessage}`);
|
||||
reject(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
let createdCollectionId = null;
|
||||
events.forEach(({ event }) => {
|
||||
if (event.section === 'nfts' && event.method === 'ForceCreated') {
|
||||
createdCollectionId = event.data[0].toNumber();
|
||||
}
|
||||
});
|
||||
|
||||
if (createdCollectionId !== null) {
|
||||
if (createdCollectionId === expectedId) {
|
||||
if (expectedId === 42) {
|
||||
console.log(` ✅ Collection #${createdCollectionId} created! 🎯 THIS IS THE TIKI COLLECTION!`);
|
||||
} else {
|
||||
console.log(` ✓ Collection #${createdCollectionId} created (placeholder)`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ⚠️ Warning: Expected #${expectedId} but got #${createdCollectionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(createdCollectionId);
|
||||
}
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
createCollection42().catch(error => {
|
||||
console.error('\n❌ Fatal error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 634 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 517 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 666 KiB |
|
After Width: | Height: | Size: 632 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 670 KiB |
|
After Width: | Height: | Size: 725 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Badge } from './badge';
|
||||
|
||||
describe('Badge Component', () => {
|
||||
it('should render the badge with the correct text', () => {
|
||||
const testMessage = 'Hello, World!';
|
||||
render(<Badge>{testMessage}</Badge>);
|
||||
|
||||
const badgeElement = screen.getByText(testMessage);
|
||||
expect(badgeElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// Commission Configuration
|
||||
|
||||
export const COMMISSIONS = {
|
||||
KYC: {
|
||||
name: 'KYC Approval Commission',
|
||||
proxyAccount: '5Hdybwv6Kbd3DJGY8DzfY4rKJWWFDPbLbuKQ81fk6eJATcTj', // KYC Commission proxy account
|
||||
threshold: 7, // 60% of 11 members
|
||||
totalMembers: 11,
|
||||
},
|
||||
// Future commissions
|
||||
VAKIF: {
|
||||
name: 'Vakıf Commission',
|
||||
proxyAccount: '', // TBD
|
||||
threshold: 5,
|
||||
totalMembers: 7,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type CommissionType = keyof typeof COMMISSIONS;
|
||||
@@ -0,0 +1,392 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useDashboard } from '@/contexts/DashboardContext';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Vote,
|
||||
Users,
|
||||
FileText,
|
||||
Scale,
|
||||
Megaphone,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
Home
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
export default function GovEntrance() {
|
||||
const { api, isApiReady, selectedAccount } = usePolkadot();
|
||||
const { nftDetails, kycStatus, loading: dashboardLoading } = useDashboard();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
checkGovernmentRole();
|
||||
}, [nftDetails, dashboardLoading]);
|
||||
|
||||
const checkGovernmentRole = () => {
|
||||
if (dashboardLoading) {
|
||||
setLoading(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user has government role NFT
|
||||
const hasGovernmentRole = nftDetails.roleNFTs.some(nft => {
|
||||
// Check if NFT is a government role (collection 42, items 10-99 are government roles)
|
||||
return nft.collectionId === 42 && nft.itemId >= 10 && nft.itemId < 100;
|
||||
});
|
||||
|
||||
if (!hasGovernmentRole) {
|
||||
toast({
|
||||
title: "Mafê Te Tuneye (No Access)",
|
||||
description: "Divê hûn xwedîyê Rola Hikûmetê bin ku vê rûpelê bigihînin (You must have a Government Role to access this page)",
|
||||
variant: "destructive"
|
||||
});
|
||||
navigate('/citizens');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleFeatureClick = (feature: string) => {
|
||||
toast({
|
||||
title: "Çalakiyê di bin nîgehdariyek de ye (Under Development)",
|
||||
description: `${feature} nûve tê avakirin (${feature} is currently under development)`,
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-700 via-white to-red-600 flex items-center justify-center">
|
||||
<Card className="bg-white/90 backdrop-blur">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-red-600"></div>
|
||||
<p className="text-gray-700 font-medium">Deriyê Hikûmetê tê barkirin... (Loading Government Portal...)</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show government portal
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-700 via-white to-red-600">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Back Button */}
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
onClick={() => navigate('/citizens')}
|
||||
variant="outline"
|
||||
className="bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Vegere Portala Welatiyên (Back to Citizens Portal)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-5xl md:text-6xl font-bold text-red-700 mb-3 drop-shadow-lg">
|
||||
🏛️ Deriyê Hikûmetê (Government Entrance)
|
||||
</h1>
|
||||
<p className="text-xl text-gray-800 font-semibold drop-shadow-md mb-2">
|
||||
Beşdariya Demokratîk (Democratic Participation)
|
||||
</p>
|
||||
<p className="text-base text-gray-700">
|
||||
Mafên xwe yên demokratîk bi kar bînin û di rêveberiya welêt de beşdar bibin
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 italic">
|
||||
(Exercise your democratic rights and participate in governance)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Features Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||
{/* 1. Elections - Hilbijartinên (Elections) */}
|
||||
<Card
|
||||
className="bg-white/95 backdrop-blur border-2 border-blue-400 hover:border-blue-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
|
||||
onClick={() => handleFeatureClick('Hilbijartinên (Elections)')}
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="bg-blue-500 w-14 h-14 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Vote className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-300">
|
||||
Aktîf (Active)
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-blue-800">Hilbijartinên</CardTitle>
|
||||
<CardDescription className="text-gray-600">(Elections & Voting)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Hilbijartina Serok (Presidential Election)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Hilbijartina Parlamentoyê (Parliamentary Elections)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Hilbijartina Serokê Meclisê (Speaker Election)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Dadgeha Destûrî (Constitutional Court)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2. Candidacy - Berjewendî (Candidacy) */}
|
||||
<Card
|
||||
className="bg-white/95 backdrop-blur border-2 border-green-400 hover:border-green-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
|
||||
onClick={() => handleFeatureClick('Berjewendî (Candidacy)')}
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="bg-green-500 w-14 h-14 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Users className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-300">
|
||||
Mafên Te (Your Rights)
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-green-800">Berjewendî</CardTitle>
|
||||
<CardDescription className="text-gray-600">(Run for Office)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<TrendingUp className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Bibe Berjewendiyê Serokbûnê (Presidential Candidate)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<TrendingUp className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Bibe Parlementêr (Parliamentary Candidate)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-xs text-yellow-700">Pêdiviya Trust Score: 300-750</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-xs text-yellow-700">Piştgiriya pêwîst: 100-1000 endorsements</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3. Proposals - Pêşniyar (Legislative Proposals) */}
|
||||
<Card
|
||||
className="bg-white/95 backdrop-blur border-2 border-purple-400 hover:border-purple-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
|
||||
onClick={() => handleFeatureClick('Pêşniyar (Proposals)')}
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="bg-purple-500 w-14 h-14 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<FileText className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-300">
|
||||
Yasayan (Legislative)
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-purple-800">Pêşniyar</CardTitle>
|
||||
<CardDescription className="text-gray-600">(Submit Proposals)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<FileText className="h-4 w-4 text-purple-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Pêşniyarên Yasayê (Legislative Proposals)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<FileText className="h-4 w-4 text-purple-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Guheztinên Destûrî (Constitutional Amendments)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<FileText className="h-4 w-4 text-purple-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Pêşniyarên Budçeyê (Budget Proposals)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 4. Voting on Proposals - Dengdayîn (Vote on Proposals) */}
|
||||
<Card
|
||||
className="bg-white/95 backdrop-blur border-2 border-orange-400 hover:border-orange-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
|
||||
onClick={() => handleFeatureClick('Dengdayîn (Voting)')}
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="bg-orange-500 w-14 h-14 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<CheckCircle className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-300">
|
||||
Parlamenter (MPs Only)
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-orange-800">Dengdayîn</CardTitle>
|
||||
<CardDescription className="text-gray-600">(Parliamentary Voting)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Erê (Aye)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<XCircle className="h-4 w-4 text-red-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Na (Nay)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<Clock className="h-4 w-4 text-gray-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Bêalî (Abstain)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<AlertCircle className="h-4 w-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-xs text-blue-700">Majority types: Simple (50%+1), Super (2/3), Absolute (3/4)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 5. Veto & Override - Veto û Têperbûn (Veto System) */}
|
||||
<Card
|
||||
className="bg-white/95 backdrop-blur border-2 border-red-400 hover:border-red-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
|
||||
onClick={() => handleFeatureClick('Veto û Têperbûn (Veto System)')}
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="bg-red-500 w-14 h-14 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Scale className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-300">
|
||||
Serok (President)
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-red-800">Veto û Têperbûn</CardTitle>
|
||||
<CardDescription className="text-gray-600">(Veto & Override)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<XCircle className="h-4 w-4 text-red-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Vetoya Serok (Presidential Veto)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-green-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Têperbûna Parlamentoyê (Parliamentary Override - 2/3)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-xs text-yellow-700">Pêdiviya 134 deng ji 201 (Requires 134 of 201 votes)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 6. Government Appointments - Tayinên Hikûmetê (Official Appointments) */}
|
||||
<Card
|
||||
className="bg-white/95 backdrop-blur border-2 border-indigo-400 hover:border-indigo-600 transition-all shadow-xl cursor-pointer group hover:scale-105"
|
||||
onClick={() => handleFeatureClick('Tayinên Hikûmetê (Appointments)')}
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="bg-indigo-500 w-14 h-14 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Megaphone className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<Badge variant="outline" className="bg-indigo-50 text-indigo-700 border-indigo-300">
|
||||
Wezîr (Ministers)
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-indigo-800">Tayinên Hikûmetê</CardTitle>
|
||||
<CardDescription className="text-gray-600">(Government Officials)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-700">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-indigo-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Wezîrên Kabîneyê (Cabinet Ministers)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="h-4 w-4 text-indigo-500 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>Dadger, Xezinedar, Noter (Judges, Treasury, Notary)</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-xs text-yellow-700">Piştgiriya Serok pêwîst e (Presidential approval required)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Status Overview */}
|
||||
<Card className="bg-gradient-to-r from-green-50 to-red-50 border-2 border-yellow-400 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl text-gray-800 flex items-center">
|
||||
<AlertCircle className="h-6 w-6 text-yellow-600 mr-2" />
|
||||
Statûya Te ya Welatîbûnê (Your Citizenship Status)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="bg-white p-4 rounded-lg border border-green-200">
|
||||
<p className="text-sm text-gray-600 mb-1">KYC Status</p>
|
||||
<div className="flex items-center">
|
||||
<Badge className="bg-green-500 text-white">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
{kycStatus}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-lg border border-blue-200">
|
||||
<p className="text-sm text-gray-600 mb-1">Mafên Dengdayînê (Voting Rights)</p>
|
||||
<div className="flex items-center">
|
||||
<Badge className="bg-blue-500 text-white">
|
||||
<Vote className="h-3 w-3 mr-1" />
|
||||
Aktîf (Active)
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-lg border border-purple-200">
|
||||
<p className="text-sm text-gray-600 mb-1">Beşdariya Rêveberiyê (Participation)</p>
|
||||
<div className="flex items-center">
|
||||
<Badge className="bg-purple-500 text-white">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
Amade (Ready)
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Bala xwe bidin (Important):</strong> Hemû mafên welatîbûnê yên te çalak in.
|
||||
Tu dikarî di hemû hilbijartinên demokratîk de beşdar bibî û deng bidî.
|
||||
<br />
|
||||
<span className="italic text-xs">
|
||||
(All your citizenship rights are active. You can participate and vote in all democratic elections.)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||