mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 20:37:56 +00:00
1295c36241
- 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
251 lines
8.2 KiB
JavaScript
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 } |