Files
pwap/backend/src/server.js
T
pezkuwichain 1295c36241 Rebrand: polkadot → pezkuwi build fixes
- Fixed TypeScript type assertion issues
- Updated imports from api-augment/substrate to api-augment/bizinikiwi
- Fixed imgConvert.mjs header and imports
- Added @ts-expect-error for runtime-converted types
- Fixed all @polkadot copyright headers to @pezkuwi
2026-01-07 02:32:54 +03:00

251 lines
8.2 KiB
JavaScript

import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import pino from 'pino'
import pinoHttp from 'pino-http'
import { createClient } from '@supabase/supabase-js'
import { ApiPromise, WsProvider, Keyring } from '@pezkuwi/api'
import { cryptoWaitReady, signatureVerify } from '@pezkuwi/util-crypto'
dotenv.config()
// ========================================
// LOGGER SETUP
// ========================================
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
...(process.env.NODE_ENV !== 'production' && {
transport: {
target: 'pino-pretty',
options: { colorize: true }
}
})
})
// ========================================
// INITIALIZATION
// ========================================
const supabaseUrl = process.env.SUPABASE_URL
const supabaseKey = process.env.SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseKey) {
logger.fatal('❌ Missing SUPABASE_URL or SUPABASE_ANON_KEY')
process.exit(1)
}
const supabase = createClient(supabaseUrl, supabaseKey)
const app = express()
app.use(cors())
app.use(express.json())
app.use(pinoHttp({ logger }))
const THRESHOLD_PERCENT = 0.6
let sudoAccount = null
let api = null
// ========================================
// BLOCKCHAIN CONNECTION
// ========================================
async function initBlockchain () {
logger.info('🔗 Connecting to Blockchain...')
const wsProvider = new WsProvider(process.env.WS_ENDPOINT || 'ws://127.0.0.1:9944')
api = await ApiPromise.create({ provider: wsProvider })
await cryptoWaitReady()
logger.info('✅ Connected to blockchain')
if (process.env.SUDO_SEED) {
const keyring = new Keyring({ type: 'sr25519' })
sudoAccount = keyring.addFromUri(process.env.SUDO_SEED)
logger.info('✅ Sudo account loaded: %s', sudoAccount.address)
} else {
logger.warn('⚠️ No SUDO_SEED found - auto-approval disabled')
}
}
// ========================================
// COUNCIL MANAGEMENT
// ========================================
app.post('/api/council/add-member', async (req, res) => {
const { newMemberAddress, signature, message } = req.body
const founderAddress = process.env.FOUNDER_ADDRESS
if (!founderAddress) {
logger.error('Founder address is not configured.')
return res.status(500).json({ error: { key: 'errors.server.founder_not_configured' } })
}
if (process.env.NODE_ENV !== 'test') {
const { isValid } = signatureVerify(message, signature, founderAddress)
if (!isValid) {
return res.status(401).json({ error: { key: 'errors.auth.invalid_signature' } })
}
if (!message.includes(`addCouncilMember:${newMemberAddress}`)) {
return res.status(400).json({ error: { key: 'errors.request.message_mismatch' } })
}
}
if (!newMemberAddress || newMemberAddress.length < 47) {
return res.status(400).json({ error: { key: 'errors.request.invalid_address' } })
}
try {
const { error } = await supabase
.from('council_members')
.insert([{ address: newMemberAddress }])
if (error) {
if (error.code === '23505') { // Unique violation
return res.status(409).json({ error: { key: 'errors.council.member_exists' } })
}
throw error
}
res.status(200).json({ success: true })
} catch (error) {
logger.error({ err: error, newMemberAddress }, 'Error adding council member')
res.status(500).json({ error: { key: 'errors.server.internal_error' } })
}
})
// ========================================
// KYC VOTING
// ========================================
app.post('/api/kyc/propose', async (req, res) => {
const { userAddress, proposerAddress, signature, message } = req.body
try {
if (process.env.NODE_ENV !== 'test') {
const { isValid } = signatureVerify(message, signature, proposerAddress)
if (!isValid) {
return res.status(401).json({ error: { key: 'errors.auth.invalid_signature' } })
}
if (!message.includes(`proposeKYC:${userAddress}`)) {
return res.status(400).json({ error: { key: 'errors.request.message_mismatch' } })
}
}
const { data: councilMember, error: memberError } = await supabase
.from('council_members').select('address').eq('address', proposerAddress).single()
if (memberError || !councilMember) {
return res.status(403).json({ error: { key: 'errors.auth.proposer_not_member' } })
}
const { error: proposalError } = await supabase
.from('kyc_proposals').insert({ user_address: userAddress, proposer_address: proposerAddress })
if (proposalError) {
if (proposalError.code === '23505') {
return res.status(409).json({ error: { key: 'errors.kyc.proposal_exists' } })
}
throw proposalError
}
const { data: proposal } = await supabase
.from('kyc_proposals').select('id').eq('user_address', userAddress).single()
await supabase.from('votes')
.insert({ proposal_id: proposal.id, voter_address: proposerAddress, is_aye: true })
await checkAndExecute(userAddress)
res.status(201).json({ success: true, proposalId: proposal.id })
} catch (error) {
logger.error({ err: error, ...req.body }, 'Error proposing KYC')
res.status(500).json({ error: { key: 'errors.server.internal_error' } })
}
})
async function checkAndExecute (userAddress) {
try {
const { count: totalMembers, error: countError } = await supabase
.from('council_members').select('*', { count: 'exact', head: true })
if (countError) throw countError
if (totalMembers === 0) return
const { data: proposal, error: proposalError } = await supabase
.from('kyc_proposals').select('id, executed').eq('user_address', userAddress).single()
if (proposalError || !proposal || proposal.executed) return
const { count: ayesCount, error: ayesError } = await supabase
.from('votes').select('*', { count: 'exact', head: true })
.eq('proposal_id', proposal.id).eq('is_aye', true)
if (ayesError) throw ayesError
const requiredVotes = Math.ceil(totalMembers * THRESHOLD_PERCENT)
if (ayesCount >= requiredVotes) {
if (!sudoAccount || !api) {
logger.error({ userAddress }, 'Cannot execute: No sudo account or API connection')
return
}
logger.info({ userAddress }, `Threshold reached! Executing approveKyc...`)
const tx = api.tx.identityKyc.approveKyc(userAddress)
await tx.signAndSend(sudoAccount, async ({ status, dispatchError, events }) => {
if (status.isFinalized) {
if (dispatchError) {
const decoded = api.registry.findMetaError(dispatchError.asModule)
const errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`
logger.error({ userAddress, error: errorMsg }, `Approval failed`)
return
}
const approvedEvent = events.find(({ event }) => api.events.identityKyc.KycApproved.is(event))
if (approvedEvent) {
logger.info({ userAddress }, 'KYC Approved on-chain. Marking as executed.')
await supabase.from('kyc_proposals').update({ executed: true }).eq('id', proposal.id)
}
}
})
}
} catch (error) {
logger.error({ err: error, userAddress }, `Error in checkAndExecute`)
}
}
// ========================================
// OTHER ENDPOINTS (GETTERS)
// ========================================
app.get('/api/kyc/pending', async (req, res) => {
try {
const { data, error } = await supabase
.from('kyc_proposals')
.select('user_address, proposer_address, created_at, votes ( voter_address, is_aye )')
.eq('executed', false)
if (error) throw error
res.json({ pending: data })
} catch (error) {
logger.error({ err: error }, 'Error fetching pending proposals')
res.status(500).json({ error: { key: 'errors.server.internal_error' } })
}
})
// ========================================
// HEALTH CHECK
// ========================================
app.get('/health', async (req, res) => {
res.json({
status: 'ok',
blockchain: api ? 'connected' : 'disconnected'
});
})
// ========================================
// START & EXPORT
// ========================================
initBlockchain().catch(error => {
logger.fatal({ err: error }, '❌ Failed to initialize blockchain')
process.exit(1)
})
export { app, supabase, api, logger }