mirror of
https://github.com/pezkuwichain/pezkuwi-mobile-app.git
synced 2026-07-16 03:15:40 +00:00
auto-commit for bed65b0f-c949-4d15-b953-0bf08c7c9e55
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* PezkuwiChain Blockchain Service
|
||||
* Handles all interactions with the PezkuwiChain blockchain via Polkadot.js
|
||||
*/
|
||||
|
||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
||||
import { CURRENT_CHAIN_CONFIG, ASSET_IDS } from '../constants/blockchain';
|
||||
import { Balance, Transaction, Proposal } from '../types';
|
||||
|
||||
class BlockchainService {
|
||||
private api: ApiPromise | null = null;
|
||||
private provider: WsProvider | null = null;
|
||||
private isConnected: boolean = false;
|
||||
|
||||
/**
|
||||
* Initialize connection to PezkuwiChain
|
||||
*/
|
||||
async connect(): Promise<boolean> {
|
||||
try {
|
||||
console.log(`Connecting to ${CURRENT_CHAIN_CONFIG.name}...`);
|
||||
console.log(`RPC URL: ${CURRENT_CHAIN_CONFIG.rpcUrl}`);
|
||||
|
||||
this.provider = new WsProvider(CURRENT_CHAIN_CONFIG.rpcUrl);
|
||||
this.api = await ApiPromise.create({ provider: this.provider });
|
||||
|
||||
await this.api.isReady;
|
||||
this.isConnected = true;
|
||||
|
||||
console.log('✅ Connected to PezkuwiChain');
|
||||
console.log(`Chain: ${await this.api.rpc.system.chain()}`);
|
||||
console.log(`Node: ${await this.api.rpc.system.name()}`);
|
||||
console.log(`Version: ${await this.api.rpc.system.version()}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to PezkuwiChain:', error);
|
||||
this.isConnected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from blockchain
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.api) {
|
||||
await this.api.disconnect();
|
||||
this.api = null;
|
||||
this.provider = null;
|
||||
this.isConnected = false;
|
||||
console.log('Disconnected from PezkuwiChain');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected to blockchain
|
||||
*/
|
||||
isApiConnected(): boolean {
|
||||
return this.isConnected && this.api !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API instance (throws if not connected)
|
||||
*/
|
||||
private getApi(): ApiPromise {
|
||||
if (!this.api || !this.isConnected) {
|
||||
throw new Error('Not connected to blockchain. Call connect() first.');
|
||||
}
|
||||
return this.api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account balances (HEZ and PEZ)
|
||||
*/
|
||||
async getBalances(address: string): Promise<Balance> {
|
||||
try {
|
||||
const api = this.getApi();
|
||||
|
||||
// Get HEZ balance (native token)
|
||||
const { data: hezBalance } = await api.query.system.account(address);
|
||||
const hezFree = hezBalance.free.toString();
|
||||
const hezReserved = hezBalance.reserved.toString();
|
||||
|
||||
// Get PEZ balance (asset)
|
||||
const pezBalance = await api.query.assets.account(ASSET_IDS.PEZ, address);
|
||||
const pezFree = pezBalance.isSome ? pezBalance.unwrap().balance.toString() : '0';
|
||||
|
||||
// Get staked HEZ
|
||||
const stakingInfo = await api.query.staking.ledger(address);
|
||||
const hezStaked = stakingInfo.isSome ? stakingInfo.unwrap().active.toString() : '0';
|
||||
|
||||
// Calculate governance power (PEZ balance as percentage)
|
||||
const totalPezSupply = '5000000000000000000000'; // 5 billion
|
||||
const governancePower = (parseFloat(pezFree) / parseFloat(totalPezSupply) * 100).toFixed(2);
|
||||
|
||||
// Mock USD values (would come from price oracle in production)
|
||||
const hezUsd = (parseFloat(hezFree) / 1e12 * 1.0).toFixed(2);
|
||||
const pezUsd = (parseFloat(pezFree) / 1e12 * 0.1).toFixed(2);
|
||||
|
||||
return {
|
||||
hez: this.formatBalance(hezFree, 12),
|
||||
pez: this.formatBalance(pezFree, 12),
|
||||
hezStaked: this.formatBalance(hezStaked, 12),
|
||||
hezUsd,
|
||||
pezUsd,
|
||||
governancePower,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching balances:', error);
|
||||
// Return mock data if blockchain not available
|
||||
return this.getMockBalances();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction history
|
||||
*/
|
||||
async getTransactions(address: string, limit: number = 10): Promise<Transaction[]> {
|
||||
try {
|
||||
// This would query blockchain events and filter for transfers
|
||||
// For now, return mock data
|
||||
return this.getMockTransactions();
|
||||
} catch (error) {
|
||||
console.error('Error fetching transactions:', error);
|
||||
return this.getMockTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active governance proposals
|
||||
*/
|
||||
async getProposals(): Promise<Proposal[]> {
|
||||
try {
|
||||
const api = this.getApi();
|
||||
|
||||
// Query welati pallet for active proposals
|
||||
const proposals = await api.query.welati.proposals.entries();
|
||||
|
||||
return proposals.map(([key, value]: any) => {
|
||||
const proposalId = key.args[0].toNumber();
|
||||
const proposal = value.unwrap();
|
||||
|
||||
return {
|
||||
id: proposalId,
|
||||
title: `Proposal ${proposalId}`,
|
||||
description: proposal.description?.toString() || 'No description',
|
||||
proposer: proposal.proposer.toString(),
|
||||
votingDeadline: proposal.deadline.toNumber(),
|
||||
yesVotes: proposal.yesVotes.toString(),
|
||||
noVotes: proposal.noVotes.toString(),
|
||||
status: proposal.status.toString() as any,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching proposals:', error);
|
||||
return this.getMockProposals();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send HEZ or PEZ tokens
|
||||
*/
|
||||
async sendTokens(
|
||||
from: string,
|
||||
to: string,
|
||||
amount: string,
|
||||
token: 'HEZ' | 'PEZ',
|
||||
signer: any
|
||||
): Promise<string> {
|
||||
try {
|
||||
const api = this.getApi();
|
||||
|
||||
let tx;
|
||||
if (token === 'HEZ') {
|
||||
tx = api.tx.balances.transfer(to, amount);
|
||||
} else {
|
||||
tx = api.tx.assets.transfer(ASSET_IDS.PEZ, to, amount);
|
||||
}
|
||||
|
||||
const hash = await tx.signAndSend(signer);
|
||||
return hash.toString();
|
||||
} catch (error) {
|
||||
console.error('Error sending tokens:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stake HEZ tokens
|
||||
*/
|
||||
async stakeTokens(
|
||||
address: string,
|
||||
amount: string,
|
||||
signer: any
|
||||
): Promise<string> {
|
||||
try {
|
||||
const api = this.getApi();
|
||||
const tx = api.tx.staking.bond(address, amount, 'Staked');
|
||||
const hash = await tx.signAndSend(signer);
|
||||
return hash.toString();
|
||||
} catch (error) {
|
||||
console.error('Error staking tokens:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vote on governance proposal
|
||||
*/
|
||||
async voteOnProposal(
|
||||
proposalId: number,
|
||||
vote: 'yes' | 'no',
|
||||
amount: string,
|
||||
signer: any
|
||||
): Promise<string> {
|
||||
try {
|
||||
const api = this.getApi();
|
||||
const tx = api.tx.welati.vote(proposalId, vote === 'yes', amount);
|
||||
const hash = await tx.signAndSend(signer);
|
||||
return hash.toString();
|
||||
} catch (error) {
|
||||
console.error('Error voting on proposal:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format balance with decimals
|
||||
*/
|
||||
private formatBalance(balance: string, decimals: number): string {
|
||||
const value = parseFloat(balance) / Math.pow(10, decimals);
|
||||
return value.toLocaleString('en-US', { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock data for development/testing
|
||||
*/
|
||||
private getMockBalances(): Balance {
|
||||
return {
|
||||
hez: '45,750.5',
|
||||
pez: '1,234,567',
|
||||
hezStaked: '30,000',
|
||||
hezUsd: '45,234',
|
||||
pezUsd: '123,456',
|
||||
governancePower: '2.5',
|
||||
};
|
||||
}
|
||||
|
||||
private getMockTransactions(): Transaction[] {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
type: 'send' as any,
|
||||
amount: '500',
|
||||
token: 'HEZ',
|
||||
from: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
|
||||
to: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
|
||||
timestamp: Date.now() - 86400000,
|
||||
blockNumber: 123456,
|
||||
hash: '0x1234567890abcdef',
|
||||
status: 'confirmed',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'receive' as any,
|
||||
amount: '300',
|
||||
token: 'PEZ',
|
||||
from: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
|
||||
to: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
|
||||
timestamp: Date.now() - 172800000,
|
||||
blockNumber: 123450,
|
||||
hash: '0xabcdef1234567890',
|
||||
status: 'confirmed',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private getMockProposals(): Proposal[] {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Proposal 1',
|
||||
description: 'Description of proposal 1',
|
||||
proposer: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
|
||||
votingDeadline: Date.now() + 172800000,
|
||||
yesVotes: '10400',
|
||||
noVotes: '4600',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Proposal 2',
|
||||
description: 'Description of proposal 2',
|
||||
proposer: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
|
||||
votingDeadline: Date.now() + 432000000,
|
||||
yesVotes: '198',
|
||||
noVotes: '0',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const blockchainService = new BlockchainService();
|
||||
export default blockchainService;
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* KYC Service
|
||||
* Handles Identity-KYC form submission, encryption, and blockchain interaction
|
||||
*/
|
||||
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { KYCFormData, KYCSubmission, KurdistanCitizen, KYCStatus } from '../types/kyc';
|
||||
import { blockchainService } from './blockchain';
|
||||
|
||||
const KYC_DATA_KEY = 'pezkuwi_kyc_data';
|
||||
const CITIZEN_DATA_KEY = 'pezkuwi_citizen_data';
|
||||
const KYC_STATUS_KEY = 'pezkuwi_kyc_status';
|
||||
|
||||
class KYCService {
|
||||
/**
|
||||
* Generate hash from KYC data
|
||||
* This hash will be submitted to blockchain
|
||||
*/
|
||||
private async generateHash(data: KYCFormData): Promise<string> {
|
||||
// In production, use proper cryptographic hash (SHA-256)
|
||||
const dataString = JSON.stringify(data);
|
||||
|
||||
// Simple hash for development (replace with proper crypto in production)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < dataString.length; i++) {
|
||||
const char = dataString.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
|
||||
return `0x${Math.abs(hash).toString(16).padStart(64, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save KYC form data locally (encrypted)
|
||||
*/
|
||||
async saveKYCData(data: KYCFormData): Promise<void> {
|
||||
try {
|
||||
const encrypted = JSON.stringify(data);
|
||||
await SecureStore.setItemAsync(KYC_DATA_KEY, encrypted);
|
||||
console.log('✅ KYC data saved locally (encrypted)');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to save KYC data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved KYC data
|
||||
*/
|
||||
async getKYCData(): Promise<KYCFormData | null> {
|
||||
try {
|
||||
const encrypted = await SecureStore.getItemAsync(KYC_DATA_KEY);
|
||||
if (!encrypted) return null;
|
||||
|
||||
return JSON.parse(encrypted);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to get KYC data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit KYC to blockchain
|
||||
* Only hash is sent, not the actual data
|
||||
*/
|
||||
async submitKYC(data: KYCFormData, signer: any): Promise<KYCSubmission> {
|
||||
try {
|
||||
// 1. Save data locally first
|
||||
await this.saveKYCData(data);
|
||||
|
||||
// 2. Generate hash
|
||||
const dataHash = await this.generateHash(data);
|
||||
console.log('📝 Generated KYC hash:', dataHash);
|
||||
|
||||
// 3. Submit hash to blockchain (identity-kyc pallet)
|
||||
// TODO: Replace with actual blockchain call when testnet is active
|
||||
const txHash = await this.submitHashToBlockchain(dataHash, signer);
|
||||
|
||||
const submission: KYCSubmission = {
|
||||
dataHash,
|
||||
submittedAt: Date.now(),
|
||||
txHash,
|
||||
};
|
||||
|
||||
// 4. Update KYC status
|
||||
await this.updateKYCStatus({ started: true, submitted: true, approved: false });
|
||||
|
||||
console.log('✅ KYC submitted to blockchain');
|
||||
return submission;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to submit KYC:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit hash to blockchain (identity-kyc pallet)
|
||||
*/
|
||||
private async submitHashToBlockchain(dataHash: string, signer: any): Promise<string> {
|
||||
try {
|
||||
// Check if blockchain is connected
|
||||
if (!blockchainService.isApiConnected()) {
|
||||
console.log('⚠️ Blockchain not connected, using mock submission');
|
||||
// Mock transaction hash for development
|
||||
return `0x${Math.random().toString(16).substr(2, 64)}`;
|
||||
}
|
||||
|
||||
// TODO: Actual blockchain submission
|
||||
// const api = blockchainService.getApi();
|
||||
// const tx = api.tx.identityKyc.submitKyc(dataHash);
|
||||
// const hash = await tx.signAndSend(signer);
|
||||
// return hash.toString();
|
||||
|
||||
// Mock for now
|
||||
return `0x${Math.random().toString(16).substr(2, 64)}`;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to submit to blockchain:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check KYC approval status on blockchain
|
||||
*/
|
||||
async checkApprovalStatus(address: string): Promise<boolean> {
|
||||
try {
|
||||
// TODO: Query blockchain for approval status
|
||||
// const api = blockchainService.getApi();
|
||||
// const approval = await api.query.identityKyc.approvals(address);
|
||||
// return approval.isSome;
|
||||
|
||||
// Mock: Auto-approve after 5 seconds (for development)
|
||||
const status = await this.getKYCStatus();
|
||||
if (status.submitted) {
|
||||
const timeSinceSubmission = Date.now() - (status.citizen?.approvedAt || 0);
|
||||
return timeSinceSubmission > 5000; // 5 seconds
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to check approval status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Kurdistan Digital Citizen certificate
|
||||
* Called after KYC is approved on blockchain
|
||||
*/
|
||||
async generateCitizen(data: KYCFormData, dataHash: string): Promise<KurdistanCitizen> {
|
||||
try {
|
||||
// Generate unique Citizen ID
|
||||
const citizenId = `KRD-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`;
|
||||
|
||||
// Generate QR code data
|
||||
const qrData = JSON.stringify({
|
||||
citizenId,
|
||||
name: data.fullName,
|
||||
region: data.region,
|
||||
hash: dataHash,
|
||||
});
|
||||
|
||||
const citizen: KurdistanCitizen = {
|
||||
citizenId,
|
||||
fullName: data.fullName,
|
||||
photo: data.photo || '',
|
||||
region: data.region,
|
||||
kycApproved: true,
|
||||
approvedAt: Date.now(),
|
||||
dataHash,
|
||||
qrCode: qrData,
|
||||
};
|
||||
|
||||
// Save citizen data locally (encrypted)
|
||||
await SecureStore.setItemAsync(CITIZEN_DATA_KEY, JSON.stringify(citizen));
|
||||
|
||||
// Update KYC status
|
||||
await this.updateKYCStatus({
|
||||
started: true,
|
||||
submitted: true,
|
||||
approved: true,
|
||||
citizen,
|
||||
});
|
||||
|
||||
console.log('✅ Kurdistan Digital Citizen generated:', citizenId);
|
||||
return citizen;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to generate citizen:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get citizen data
|
||||
*/
|
||||
async getCitizen(): Promise<KurdistanCitizen | null> {
|
||||
try {
|
||||
const data = await SecureStore.getItemAsync(CITIZEN_DATA_KEY);
|
||||
if (!data) return null;
|
||||
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to get citizen data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KYC status
|
||||
*/
|
||||
async getKYCStatus(): Promise<KYCStatus> {
|
||||
try {
|
||||
const statusData = await SecureStore.getItemAsync(KYC_STATUS_KEY);
|
||||
if (!statusData) {
|
||||
return { started: false, submitted: false, approved: false };
|
||||
}
|
||||
|
||||
return JSON.parse(statusData);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to get KYC status:', error);
|
||||
return { started: false, submitted: false, approved: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update KYC status
|
||||
*/
|
||||
private async updateKYCStatus(status: KYCStatus): Promise<void> {
|
||||
try {
|
||||
await SecureStore.setItemAsync(KYC_STATUS_KEY, JSON.stringify(status));
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to update KYC status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has access to Governance
|
||||
*/
|
||||
async hasGovernanceAccess(): Promise<boolean> {
|
||||
const status = await this.getKYCStatus();
|
||||
return status.approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all KYC data (for testing)
|
||||
*/
|
||||
async clearKYCData(): Promise<void> {
|
||||
try {
|
||||
await SecureStore.deleteItemAsync(KYC_DATA_KEY);
|
||||
await SecureStore.deleteItemAsync(CITIZEN_DATA_KEY);
|
||||
await SecureStore.deleteItemAsync(KYC_STATUS_KEY);
|
||||
console.log('✅ KYC data cleared');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to clear KYC data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const kycService = new KYCService();
|
||||
export default kycService;
|
||||
|
||||
Reference in New Issue
Block a user