mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 02:07:55 +00:00
feat(core): Add backend services, scripts, and initial test structure
This commit is contained in:
@@ -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
|
||||
Generated
+2153
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user