feat(core): Add backend services, scripts, and initial test structure

This commit is contained in:
2025-11-19 18:48:54 +03:00
parent 703e11711e
commit bdf59cea47
51 changed files with 3460 additions and 0 deletions
+9
View File
@@ -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
+350
View File
@@ -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
+2153
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -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"
}
}
+372
View File
@@ -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);
});
+129
View File
@@ -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);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+13
View File
@@ -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();
});
});
+19
View File
@@ -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;
+392
View File
@@ -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ê 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>
);
}
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom';