Files
pwap/shared/lib/validator-pool.ts
T

376 lines
10 KiB
TypeScript

import { ApiPromise } from '@pezkuwi/api';
import { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
import { toast } from 'sonner';
/**
* Validator pool categories from runtime
*/
export enum ValidatorPoolCategory {
StakeValidator = 'StakeValidator',
ParliamentaryValidator = 'ParliamentaryValidator',
MeritValidator = 'MeritValidator',
}
/**
* Pool member information
*/
export interface PoolMember {
address: string;
category: ValidatorPoolCategory;
joinedAt: number;
}
/**
* Validator set structure
*/
export interface ValidatorSet {
stake_validators: string[];
parliamentary_validators: string[];
merit_validators: string[];
total_count: number;
}
/**
* Performance metrics
*/
export interface PerformanceMetrics {
blocks_produced: number;
blocks_missed: number;
era_points: number;
reputation_score: number;
}
/**
* Join validator pool with specified category
*/
export async function joinValidatorPool(
api: ApiPromise,
account: InjectedAccountWithMeta,
category: ValidatorPoolCategory
): Promise<void> {
try {
// Convert category to runtime enum
const categoryEnum = { [category]: null };
const extrinsic = api.tx.validatorPool.joinValidatorPool(categoryEnum);
await new Promise<void>((resolve, reject) => {
let unsub: () => void;
extrinsic.signAndSend(
account.address,
({ status, dispatchError }) => {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
const errorMsg = `${decoded.section}.${decoded.name}`;
// User-friendly error messages
if (errorMsg === 'validatorPool.InsufficientTrustScore') {
reject(new Error('Insufficient trust score. Minimum 500 required.'));
} else if (errorMsg === 'validatorPool.AlreadyInPool') {
reject(new Error('Already in validator pool'));
} else if (errorMsg === 'validatorPool.MissingRequiredTiki') {
reject(new Error('Missing required Tiki citizenship for this category'));
} else {
reject(new Error(errorMsg));
}
} else {
reject(new Error(dispatchError.toString()));
}
if (unsub) unsub();
return;
}
if (status.isInBlock || status.isFinalized) {
toast.success(`Joined ${category} pool successfully`);
resolve();
if (unsub) unsub();
}
}
).then((unsubscribe) => {
unsub = unsubscribe;
});
});
} catch (error) {
console.error('Join validator pool error:', error);
toast.error(error instanceof Error ? error.message : 'Failed to join validator pool');
throw error;
}
}
/**
* Leave validator pool
*/
export async function leaveValidatorPool(
api: ApiPromise,
account: InjectedAccountWithMeta
): Promise<void> {
try {
const extrinsic = api.tx.validatorPool.leaveValidatorPool();
await new Promise<void>((resolve, reject) => {
let unsub: () => void;
extrinsic.signAndSend(
account.address,
({ status, dispatchError }) => {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
const errorMsg = `${decoded.section}.${decoded.name}`;
if (errorMsg === 'validatorPool.NotInPool') {
reject(new Error('Not currently in validator pool'));
} else {
reject(new Error(errorMsg));
}
} else {
reject(new Error(dispatchError.toString()));
}
if (unsub) unsub();
return;
}
if (status.isInBlock || status.isFinalized) {
toast.success('Left validator pool successfully');
resolve();
if (unsub) unsub();
}
}
).then((unsubscribe) => {
unsub = unsubscribe;
});
});
} catch (error) {
console.error('Leave validator pool error:', error);
toast.error(error instanceof Error ? error.message : 'Failed to leave validator pool');
throw error;
}
}
/**
* Update validator category
*/
export async function updateValidatorCategory(
api: ApiPromise,
account: InjectedAccountWithMeta,
newCategory: ValidatorPoolCategory
): Promise<void> {
try {
const categoryEnum = { [newCategory]: null };
const extrinsic = api.tx.validatorPool.updateCategory(categoryEnum);
await new Promise<void>((resolve, reject) => {
let unsub: () => void;
extrinsic.signAndSend(
account.address,
({ status, dispatchError }) => {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}`));
} else {
reject(new Error(dispatchError.toString()));
}
if (unsub) unsub();
return;
}
if (status.isInBlock || status.isFinalized) {
toast.success(`Category updated to ${newCategory}`);
resolve();
if (unsub) unsub();
}
}
).then((unsubscribe) => {
unsub = unsubscribe;
});
});
} catch (error) {
console.error('Update category error:', error);
toast.error(error instanceof Error ? error.message : 'Failed to update category');
throw error;
}
}
/**
* Get validator pool member info
*/
export async function getPoolMember(
api: ApiPromise,
address: string
): Promise<ValidatorPoolCategory | null> {
try {
const member = await api.query.validatorPool.poolMembers(address);
if (member.isNone) {
return null;
}
const category = member.unwrap() as any;
// Parse category enum
if (category.isStakeValidator) {
return ValidatorPoolCategory.StakeValidator;
} else if (category.isParliamentaryValidator) {
return ValidatorPoolCategory.ParliamentaryValidator;
} else if (category.isMeritValidator) {
return ValidatorPoolCategory.MeritValidator;
}
return null;
} catch (error) {
console.error('Get pool member error:', error);
return null;
}
}
/**
* Get total pool size
*/
export async function getPoolSize(api: ApiPromise): Promise<number> {
try {
const size = await api.query.validatorPool.poolSize();
return size.toNumber();
} catch (error) {
console.error('Get pool size error:', error);
return 0;
}
}
/**
* Get current validator set
*/
export async function getCurrentValidatorSet(api: ApiPromise): Promise<ValidatorSet | null> {
try {
const validatorSet = await api.query.validatorPool.currentValidatorSet();
if (validatorSet.isNone) {
return null;
}
const set = validatorSet.unwrap() as any;
return {
stake_validators: set.stakeValidators.map((v: any) => v.toString()),
parliamentary_validators: set.parliamentaryValidators.map((v: any) => v.toString()),
merit_validators: set.meritValidators.map((v: any) => v.toString()),
total_count: set.totalCount.toNumber(),
};
} catch (error) {
console.error('Get validator set error:', error);
return null;
}
}
/**
* Get current era
*/
export async function getCurrentEra(api: ApiPromise): Promise<number> {
try {
const era = await api.query.validatorPool.currentEra();
return era.toNumber();
} catch (error) {
console.error('Get current era error:', error);
return 0;
}
}
/**
* Get performance metrics for a validator
*/
export async function getPerformanceMetrics(
api: ApiPromise,
address: string
): Promise<PerformanceMetrics> {
try {
const metrics = await api.query.validatorPool.performanceMetrics(address) as any;
return {
blocks_produced: metrics.blocksProduced.toNumber(),
blocks_missed: metrics.blocksMissed.toNumber(),
era_points: metrics.eraPoints.toNumber(),
reputation_score: metrics.reputationScore.toNumber(),
};
} catch (error) {
console.error('Get performance metrics error:', error);
return {
blocks_produced: 0,
blocks_missed: 0,
era_points: 0,
reputation_score: 0,
};
}
}
/**
* Get all pool members (requires iterating storage)
*/
export async function getAllPoolMembers(api: ApiPromise): Promise<PoolMember[]> {
try {
const entries = await api.query.validatorPool.poolMembers.entries();
const members: PoolMember[] = entries.map(([key, value]) => {
const address = key.args[0].toString();
const category = value.unwrap() as any;
let categoryType: ValidatorPoolCategory;
if (category.isStakeValidator) {
categoryType = ValidatorPoolCategory.StakeValidator;
} else if (category.isParliamentaryValidator) {
categoryType = ValidatorPoolCategory.ParliamentaryValidator;
} else {
categoryType = ValidatorPoolCategory.MeritValidator;
}
return {
address,
category: categoryType,
joinedAt: 0, // Block number not stored in this version
};
});
return members;
} catch (error) {
console.error('Get all pool members error:', error);
return [];
}
}
/**
* Check if address meets requirements for category
*/
export async function checkCategoryRequirements(
api: ApiPromise,
address: string,
category: ValidatorPoolCategory
): Promise<{ eligible: boolean; reason?: string }> {
try {
// Get trust score
const trustScore = await api.query.trust.trustScores(address);
const score = trustScore.toNumber();
if (score < 500) {
return { eligible: false, reason: 'Trust score below 500' };
}
// Check Tiki for Parliamentary/Merit validators
if (
category === ValidatorPoolCategory.ParliamentaryValidator ||
category === ValidatorPoolCategory.MeritValidator
) {
const tikiScore = await api.query.tiki.tikiScores(address);
if (tikiScore.isNone || (tikiScore.unwrap() as any).toNumber() === 0) {
return { eligible: false, reason: 'Tiki citizenship required' };
}
}
return { eligible: true };
} catch (error) {
console.error('Check category requirements error:', error);
return { eligible: false, reason: 'Failed to check requirements' };
}
}