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);
});