mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 13:37:59 +00:00
Centralize common code in shared folder
This commit reorganizes the codebase to eliminate duplication between web and mobile frontends by moving all commonly used files to the shared folder. Changes: - Moved lib files to shared/lib/: * wallet.ts, staking.ts, tiki.ts, identity.ts * multisig.ts, usdt.ts, scores.ts, citizenship-workflow.ts - Moved utils to shared/utils/: * auth.ts, dex.ts * Created format.ts (extracted formatNumber from web utils) - Created shared/theme/: * colors.ts (Kurdistan and App color definitions) - Updated web configuration: * Added @pezkuwi/* path aliases in tsconfig.json and vite.config.ts * Updated all imports to use @pezkuwi/lib/*, @pezkuwi/utils/*, @pezkuwi/theme/* * Removed duplicate files from web/src/lib and web/src/utils - Updated mobile configuration: * Added @pezkuwi/* path aliases in tsconfig.json * Updated theme/colors.ts to re-export from shared * Mobile already uses relative imports to shared (no changes needed) Architecture Benefits: - Single source of truth for common code - No duplication between frontends - Easier maintenance and consistency - Clear separation of shared vs platform-specific code Web-specific files kept: - web/src/lib/supabase.ts - web/src/lib/utils.ts (cn function for Tailwind, re-exports formatNumber from shared) All imports updated and tested. Both web and mobile now use the centralized shared folder.
This commit is contained in:
@@ -1,404 +0,0 @@
|
||||
// ========================================
|
||||
// Citizenship Crypto Utilities
|
||||
// ========================================
|
||||
// Handles encryption, hashing, signatures for citizenship data
|
||||
|
||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||
import { stringToHex, hexToU8a, u8aToHex, stringToU8a } from '@polkadot/util';
|
||||
import { decodeAddress, signatureVerify, cryptoWaitReady } from '@polkadot/util-crypto';
|
||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||
import type { CitizenshipData } from './citizenship-workflow';
|
||||
|
||||
// ========================================
|
||||
// HASHING FUNCTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Generate SHA-256 hash from data
|
||||
*/
|
||||
export async function generateHash(data: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const dataBuffer = encoder.encode(data);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return `0x${hashHex}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate commitment hash from citizenship data
|
||||
*/
|
||||
export async function generateCommitmentHash(
|
||||
data: CitizenshipData
|
||||
): Promise<string> {
|
||||
const dataString = JSON.stringify({
|
||||
fullName: data.fullName,
|
||||
fatherName: data.fatherName,
|
||||
grandfatherName: data.grandfatherName,
|
||||
motherName: data.motherName,
|
||||
tribe: data.tribe,
|
||||
maritalStatus: data.maritalStatus,
|
||||
childrenCount: data.childrenCount,
|
||||
children: data.children,
|
||||
region: data.region,
|
||||
email: data.email,
|
||||
profession: data.profession,
|
||||
referralCode: data.referralCode,
|
||||
walletAddress: data.walletAddress,
|
||||
timestamp: data.timestamp
|
||||
});
|
||||
|
||||
return generateHash(dataString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate nullifier hash (prevents double-registration)
|
||||
*/
|
||||
export async function generateNullifierHash(
|
||||
walletAddress: string,
|
||||
timestamp: number
|
||||
): Promise<string> {
|
||||
const nullifierData = `${walletAddress}-${timestamp}-nullifier`;
|
||||
return generateHash(nullifierData);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ENCRYPTION / DECRYPTION (AES-GCM)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Derive encryption key from wallet address
|
||||
* NOTE: For MVP, we use a deterministic key. For production, use proper key derivation
|
||||
*/
|
||||
async function deriveEncryptionKey(walletAddress: string): Promise<CryptoKey> {
|
||||
// Create a deterministic seed from wallet address
|
||||
const seed = await generateHash(walletAddress);
|
||||
|
||||
// Convert hex to ArrayBuffer
|
||||
const keyMaterial = hexToU8a(seed).slice(0, 32); // 256-bit key
|
||||
|
||||
// Import as AES-GCM key
|
||||
return crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt citizenship data
|
||||
*/
|
||||
export async function encryptData(
|
||||
data: CitizenshipData,
|
||||
walletAddress: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const key = await deriveEncryptionKey(walletAddress);
|
||||
|
||||
// Generate random IV (Initialization Vector)
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// Encrypt data
|
||||
const encoder = new TextEncoder();
|
||||
const dataBuffer = encoder.encode(JSON.stringify(data));
|
||||
|
||||
const encryptedBuffer = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
dataBuffer
|
||||
);
|
||||
|
||||
// Combine IV + encrypted data
|
||||
const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength);
|
||||
combined.set(iv, 0);
|
||||
combined.set(new Uint8Array(encryptedBuffer), iv.length);
|
||||
|
||||
// Convert to hex
|
||||
return u8aToHex(combined);
|
||||
} catch (error) {
|
||||
console.error('Encryption error:', error);
|
||||
throw new Error('Failed to encrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt citizenship data
|
||||
*/
|
||||
export async function decryptData(
|
||||
encryptedHex: string,
|
||||
walletAddress: string
|
||||
): Promise<CitizenshipData> {
|
||||
try {
|
||||
const key = await deriveEncryptionKey(walletAddress);
|
||||
|
||||
// Convert hex to Uint8Array
|
||||
const combined = hexToU8a(encryptedHex);
|
||||
|
||||
// Extract IV and encrypted data
|
||||
const iv = combined.slice(0, 12);
|
||||
const encryptedData = combined.slice(12);
|
||||
|
||||
// Decrypt
|
||||
const decryptedBuffer = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encryptedData
|
||||
);
|
||||
|
||||
// Convert to string and parse JSON
|
||||
const decoder = new TextDecoder();
|
||||
const decryptedString = decoder.decode(decryptedBuffer);
|
||||
|
||||
return JSON.parse(decryptedString) as CitizenshipData;
|
||||
} catch (error) {
|
||||
console.error('Decryption error:', error);
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SIGNATURE GENERATION & VERIFICATION
|
||||
// ========================================
|
||||
|
||||
export interface AuthChallenge {
|
||||
nonce: string; // Random UUID
|
||||
timestamp: number; // Current timestamp
|
||||
tikiNumber: string; // NFT number to prove
|
||||
expiresAt: number; // Expiry timestamp (5 min)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate authentication challenge
|
||||
*/
|
||||
export function generateAuthChallenge(tikiNumber: string): AuthChallenge {
|
||||
const now = Date.now();
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
return {
|
||||
nonce,
|
||||
timestamp: now,
|
||||
tikiNumber,
|
||||
expiresAt: now + (5 * 60 * 1000) // 5 minutes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format challenge message for signing
|
||||
*/
|
||||
export function formatChallengeMessage(challenge: AuthChallenge): string {
|
||||
return `Prove ownership of Welati Tiki #${challenge.tikiNumber}
|
||||
|
||||
Nonce: ${challenge.nonce}
|
||||
Timestamp: ${challenge.timestamp}
|
||||
Expires: ${new Date(challenge.expiresAt).toISOString()}
|
||||
|
||||
By signing this message, you prove you control the wallet that owns this Tiki NFT.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign authentication challenge with wallet
|
||||
*/
|
||||
export async function signChallenge(
|
||||
account: InjectedAccountWithMeta,
|
||||
challenge: AuthChallenge
|
||||
): Promise<string> {
|
||||
try {
|
||||
await cryptoWaitReady();
|
||||
|
||||
const injector = await web3FromAddress(account.address);
|
||||
const signRaw = injector?.signer?.signRaw;
|
||||
|
||||
if (!signRaw) {
|
||||
throw new Error('Signer not available');
|
||||
}
|
||||
|
||||
const message = formatChallengeMessage(challenge);
|
||||
|
||||
const { signature } = await signRaw({
|
||||
address: account.address,
|
||||
data: stringToHex(message),
|
||||
type: 'bytes'
|
||||
});
|
||||
|
||||
return signature;
|
||||
} catch (error) {
|
||||
console.error('Signature error:', error);
|
||||
throw new Error('Failed to sign challenge');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify signature
|
||||
*/
|
||||
export async function verifySignature(
|
||||
signature: string,
|
||||
challenge: AuthChallenge,
|
||||
expectedAddress: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await cryptoWaitReady();
|
||||
|
||||
// Check if challenge has expired
|
||||
if (Date.now() > challenge.expiresAt) {
|
||||
console.warn('Challenge has expired');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = formatChallengeMessage(challenge);
|
||||
const messageU8a = stringToU8a(message);
|
||||
const signatureU8a = hexToU8a(signature);
|
||||
const publicKey = decodeAddress(expectedAddress);
|
||||
|
||||
const result = signatureVerify(messageU8a, signatureU8a, publicKey);
|
||||
|
||||
return result.isValid;
|
||||
} catch (error) {
|
||||
console.error('Verification error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// LOCAL STORAGE UTILITIES
|
||||
// ========================================
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'pezkuwi_citizen_';
|
||||
|
||||
export interface CitizenSession {
|
||||
tikiNumber: string;
|
||||
walletAddress: string;
|
||||
sessionToken: string; // JWT-like token
|
||||
encryptedDataCID?: string; // IPFS CID
|
||||
lastAuthenticated: number; // Timestamp
|
||||
expiresAt: number; // Session expiry (24h)
|
||||
}
|
||||
|
||||
/**
|
||||
* Save encrypted citizen session to localStorage
|
||||
*/
|
||||
export async function saveCitizenSession(session: CitizenSession): Promise<void> {
|
||||
try {
|
||||
const sessionJson = JSON.stringify(session);
|
||||
const sessionKey = `${STORAGE_KEY_PREFIX}session`;
|
||||
|
||||
// For MVP, store plainly. For production, encrypt with device key
|
||||
localStorage.setItem(sessionKey, sessionJson);
|
||||
} catch (error) {
|
||||
console.error('Error saving session:', error);
|
||||
throw new Error('Failed to save session');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load citizen session from localStorage
|
||||
*/
|
||||
export function loadCitizenSession(): CitizenSession | null {
|
||||
try {
|
||||
const sessionKey = `${STORAGE_KEY_PREFIX}session`;
|
||||
const sessionJson = localStorage.getItem(sessionKey);
|
||||
|
||||
if (!sessionJson) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = JSON.parse(sessionJson) as CitizenSession;
|
||||
|
||||
// Check if session has expired
|
||||
if (Date.now() > session.expiresAt) {
|
||||
clearCitizenSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error('Error loading session:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear citizen session from localStorage
|
||||
*/
|
||||
export function clearCitizenSession(): void {
|
||||
try {
|
||||
const sessionKey = `${STORAGE_KEY_PREFIX}session`;
|
||||
localStorage.removeItem(sessionKey);
|
||||
} catch (error) {
|
||||
console.error('Error clearing session:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save encrypted citizenship data to localStorage (backup)
|
||||
*/
|
||||
export async function saveLocalCitizenshipData(
|
||||
data: CitizenshipData,
|
||||
walletAddress: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const encrypted = await encryptData(data, walletAddress);
|
||||
const dataKey = `${STORAGE_KEY_PREFIX}data_${walletAddress}`;
|
||||
|
||||
localStorage.setItem(dataKey, encrypted);
|
||||
} catch (error) {
|
||||
console.error('Error saving citizenship data:', error);
|
||||
throw new Error('Failed to save citizenship data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load encrypted citizenship data from localStorage
|
||||
*/
|
||||
export async function loadLocalCitizenshipData(
|
||||
walletAddress: string
|
||||
): Promise<CitizenshipData | null> {
|
||||
try {
|
||||
const dataKey = `${STORAGE_KEY_PREFIX}data_${walletAddress}`;
|
||||
const encrypted = localStorage.getItem(dataKey);
|
||||
|
||||
if (!encrypted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await decryptData(encrypted, walletAddress);
|
||||
} catch (error) {
|
||||
console.error('Error loading citizenship data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IPFS UTILITIES (Placeholder)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Upload encrypted data to IPFS
|
||||
* NOTE: This is a placeholder. Implement with actual IPFS client (Pinata, Web3.Storage, etc.)
|
||||
*/
|
||||
export async function uploadToIPFS(encryptedData: string): Promise<string> {
|
||||
// TODO: Implement actual IPFS upload
|
||||
// For MVP, we can use Pinata API or Web3.Storage
|
||||
|
||||
console.warn('IPFS upload not yet implemented. Using mock CID.');
|
||||
|
||||
// Mock CID for development
|
||||
const mockCid = `Qm${Math.random().toString(36).substring(2, 15)}`;
|
||||
|
||||
return mockCid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch encrypted data from IPFS
|
||||
* NOTE: This is a placeholder. Implement with actual IPFS client
|
||||
*/
|
||||
export async function fetchFromIPFS(cid: string): Promise<string> {
|
||||
// TODO: Implement actual IPFS fetch
|
||||
// For MVP, use public IPFS gateways or dedicated service
|
||||
|
||||
console.warn('IPFS fetch not yet implemented. Returning mock data.');
|
||||
|
||||
// Mock encrypted data
|
||||
return '0x000000000000000000000000';
|
||||
}
|
||||
@@ -1,624 +0,0 @@
|
||||
// ========================================
|
||||
// Citizenship Workflow Library
|
||||
// ========================================
|
||||
// Handles citizenship verification, status checks, and workflow logic
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||
|
||||
// ========================================
|
||||
// TYPE DEFINITIONS
|
||||
// ========================================
|
||||
|
||||
export type KycStatus = 'NotStarted' | 'Pending' | 'Approved' | 'Rejected';
|
||||
|
||||
export type Region =
|
||||
| 'bakur' // North (Turkey)
|
||||
| 'basur' // South (Iraq)
|
||||
| 'rojava' // West (Syria)
|
||||
| 'rojhelat' // East (Iran)
|
||||
| 'diaspora' // Diaspora
|
||||
| 'kurdistan_a_sor'; // Red Kurdistan (Armenia/Azerbaijan)
|
||||
|
||||
export type MaritalStatus = 'zewici' | 'nezewici'; // Married / Unmarried
|
||||
|
||||
export interface ChildInfo {
|
||||
name: string;
|
||||
birthYear: number;
|
||||
}
|
||||
|
||||
export interface CitizenshipData {
|
||||
// Personal Identity
|
||||
fullName: string;
|
||||
fatherName: string;
|
||||
grandfatherName: string;
|
||||
motherName: string;
|
||||
|
||||
// Tribal Affiliation
|
||||
tribe: string;
|
||||
|
||||
// Family Status
|
||||
maritalStatus: MaritalStatus;
|
||||
childrenCount?: number;
|
||||
children?: ChildInfo[];
|
||||
|
||||
// Geographic Origin
|
||||
region: Region;
|
||||
|
||||
// Contact & Profession
|
||||
email: string;
|
||||
profession: string;
|
||||
|
||||
// Referral
|
||||
referralCode?: string;
|
||||
|
||||
// Metadata
|
||||
walletAddress: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface CitizenshipCommitment {
|
||||
commitmentHash: string; // SHA256 hash of all data
|
||||
nullifierHash: string; // Prevents double-registration
|
||||
ipfsCid: string; // IPFS CID of encrypted data
|
||||
publicKey: string; // User's encryption public key
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface TikiInfo {
|
||||
id: string;
|
||||
role: string;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
export interface CitizenshipStatus {
|
||||
kycStatus: KycStatus;
|
||||
hasCitizenTiki: boolean;
|
||||
tikiNumber?: string;
|
||||
stakingScoreTracking: boolean;
|
||||
ipfsCid?: string;
|
||||
nextAction: 'APPLY_KYC' | 'CLAIM_TIKI' | 'START_TRACKING' | 'COMPLETE';
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// KYC STATUS CHECKS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get KYC status for a wallet address
|
||||
*/
|
||||
export async function getKycStatus(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<KycStatus> {
|
||||
try {
|
||||
if (!api?.query?.identityKyc) {
|
||||
console.warn('Identity KYC pallet not available');
|
||||
return 'NotStarted';
|
||||
}
|
||||
|
||||
const status = await api.query.identityKyc.kycStatuses(address);
|
||||
|
||||
if (status.isEmpty) {
|
||||
return 'NotStarted';
|
||||
}
|
||||
|
||||
const statusStr = status.toString();
|
||||
|
||||
// Map on-chain status to our type
|
||||
if (statusStr === 'Approved') return 'Approved';
|
||||
if (statusStr === 'Pending') return 'Pending';
|
||||
if (statusStr === 'Rejected') return 'Rejected';
|
||||
|
||||
return 'NotStarted';
|
||||
} catch (error) {
|
||||
console.error('Error fetching KYC status:', error);
|
||||
return 'NotStarted';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has pending KYC application
|
||||
*/
|
||||
export async function hasPendingApplication(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!api?.query?.identityKyc?.pendingKycApplications) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const application = await api.query.identityKyc.pendingKycApplications(address);
|
||||
return !application.isEmpty;
|
||||
} catch (error) {
|
||||
console.error('Error checking pending application:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TIKI / CITIZENSHIP CHECKS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get all Tiki roles for a user
|
||||
*/
|
||||
// Tiki enum mapping from pallet-tiki
|
||||
const TIKI_ROLES = [
|
||||
'Hemwelatî', 'Parlementer', 'SerokiMeclise', 'Serok', 'Wezir', 'EndameDiwane', 'Dadger',
|
||||
'Dozger', 'Hiquqnas', 'Noter', 'Xezinedar', 'Bacgir', 'GerinendeyeCavkaniye', 'OperatorêTorê',
|
||||
'PisporêEwlehiyaSîber', 'GerinendeyeDaneye', 'Berdevk', 'Qeydkar', 'Balyoz', 'Navbeynkar',
|
||||
'ParêzvaneÇandî', 'Mufetîs', 'KalîteKontrolker', 'Mela', 'Feqî', 'Perwerdekar', 'Rewsenbîr',
|
||||
'RêveberêProjeyê', 'SerokêKomele', 'ModeratorêCivakê', 'Axa', 'Pêseng', 'Sêwirmend', 'Hekem', 'Mamoste',
|
||||
'Bazargan',
|
||||
'SerokWeziran', 'WezireDarayiye', 'WezireParez', 'WezireDad', 'WezireBelaw', 'WezireTend', 'WezireAva', 'WezireCand'
|
||||
];
|
||||
|
||||
export async function getUserTikis(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<TikiInfo[]> {
|
||||
try {
|
||||
if (!api?.query?.tiki?.userTikis) {
|
||||
console.warn('Tiki pallet not available');
|
||||
return [];
|
||||
}
|
||||
|
||||
const tikis = await api.query.tiki.userTikis(address);
|
||||
|
||||
if (tikis.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// userTikis returns a BoundedVec of Tiki enum values (as indices)
|
||||
const tikiIndices = tikis.toJSON() as number[];
|
||||
|
||||
return tikiIndices.map((index, i) => ({
|
||||
id: `${index}`,
|
||||
role: TIKI_ROLES[index] || `Unknown Role (${index})`,
|
||||
metadata: {}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching user tikis:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has Welati (Citizen) Tiki
|
||||
* Backend checks for "Hemwelatî" (actual blockchain role name)
|
||||
*/
|
||||
export async function hasCitizenTiki(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<{ hasTiki: boolean; tikiNumber?: string }> {
|
||||
try {
|
||||
const tikis = await getUserTikis(api, address);
|
||||
|
||||
const citizenTiki = tikis.find(t =>
|
||||
t.role.toLowerCase() === 'hemwelatî' ||
|
||||
t.role.toLowerCase() === 'welati' ||
|
||||
t.role.toLowerCase() === 'citizen'
|
||||
);
|
||||
|
||||
return {
|
||||
hasTiki: !!citizenTiki,
|
||||
tikiNumber: citizenTiki?.id
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking citizen tiki:', error);
|
||||
return { hasTiki: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify NFT ownership by NFT number
|
||||
*/
|
||||
export async function verifyNftOwnership(
|
||||
api: ApiPromise,
|
||||
nftNumber: string,
|
||||
walletAddress: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const tikis = await getUserTikis(api, walletAddress);
|
||||
|
||||
return tikis.some(tiki =>
|
||||
tiki.id === nftNumber &&
|
||||
(
|
||||
tiki.role.toLowerCase() === 'hemwelatî' ||
|
||||
tiki.role.toLowerCase() === 'welati' ||
|
||||
tiki.role.toLowerCase() === 'citizen'
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error verifying NFT ownership:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STAKING SCORE TRACKING
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if staking score tracking has been started
|
||||
*/
|
||||
export async function isStakingScoreTracking(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!api?.query?.stakingScore?.stakingStartBlock) {
|
||||
console.warn('Staking score pallet not available');
|
||||
return false;
|
||||
}
|
||||
|
||||
const startBlock = await api.query.stakingScore.stakingStartBlock(address);
|
||||
return !startBlock.isNone;
|
||||
} catch (error) {
|
||||
console.error('Error checking staking score tracking:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is staking
|
||||
*/
|
||||
export async function isStaking(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!api?.query?.staking?.ledger) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ledger = await api.query.staking.ledger(address);
|
||||
return !ledger.isNone;
|
||||
} catch (error) {
|
||||
console.error('Error checking staking status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COMPREHENSIVE CITIZENSHIP STATUS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get complete citizenship status and next action needed
|
||||
*/
|
||||
export async function getCitizenshipStatus(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<CitizenshipStatus> {
|
||||
try {
|
||||
if (!api || !address) {
|
||||
return {
|
||||
kycStatus: 'NotStarted',
|
||||
hasCitizenTiki: false,
|
||||
stakingScoreTracking: false,
|
||||
nextAction: 'APPLY_KYC'
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch all status in parallel
|
||||
const [kycStatus, citizenCheck, stakingTracking] = await Promise.all([
|
||||
getKycStatus(api, address),
|
||||
hasCitizenTiki(api, address),
|
||||
isStakingScoreTracking(api, address)
|
||||
]);
|
||||
|
||||
const kycApproved = kycStatus === 'Approved';
|
||||
const hasTiki = citizenCheck.hasTiki;
|
||||
|
||||
// Determine next action
|
||||
let nextAction: CitizenshipStatus['nextAction'];
|
||||
|
||||
if (!kycApproved) {
|
||||
nextAction = 'APPLY_KYC';
|
||||
} else if (!hasTiki) {
|
||||
nextAction = 'CLAIM_TIKI';
|
||||
} else if (!stakingTracking) {
|
||||
nextAction = 'START_TRACKING';
|
||||
} else {
|
||||
nextAction = 'COMPLETE';
|
||||
}
|
||||
|
||||
return {
|
||||
kycStatus,
|
||||
hasCitizenTiki: hasTiki,
|
||||
tikiNumber: citizenCheck.tikiNumber,
|
||||
stakingScoreTracking: stakingTracking,
|
||||
nextAction
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching citizenship status:', error);
|
||||
return {
|
||||
kycStatus: 'NotStarted',
|
||||
hasCitizenTiki: false,
|
||||
stakingScoreTracking: false,
|
||||
nextAction: 'APPLY_KYC'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IPFS COMMITMENT RETRIEVAL
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get IPFS CID for citizen data
|
||||
*/
|
||||
export async function getCitizenDataCid(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
if (!api?.query?.identityKyc?.identities) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to get from identity storage
|
||||
// This assumes the pallet stores IPFS CID somewhere
|
||||
// Adjust based on actual pallet storage structure
|
||||
const identity = await api.query.identityKyc.identities(address);
|
||||
|
||||
if (identity.isNone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const identityData = identity.unwrap().toJSON() as any;
|
||||
|
||||
// Try different possible field names
|
||||
return identityData.ipfsCid ||
|
||||
identityData.cid ||
|
||||
identityData.dataCid ||
|
||||
null;
|
||||
} catch (error) {
|
||||
console.error('Error fetching citizen data CID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// REFERRAL VALIDATION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Validate referral code
|
||||
*/
|
||||
export async function validateReferralCode(
|
||||
api: ApiPromise,
|
||||
referralCode: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
if (!referralCode || referralCode.trim() === '') {
|
||||
return true; // Empty is valid (will use founder)
|
||||
}
|
||||
|
||||
// Check if referral code exists in trust pallet
|
||||
if (!api?.query?.trust?.referrals) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Referral code could be an address or custom code
|
||||
// For now, check if it's a valid address format
|
||||
// TODO: Implement proper referral code lookup
|
||||
|
||||
return referralCode.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error validating referral code:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BLOCKCHAIN TRANSACTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Submit KYC application to blockchain
|
||||
* This is a two-step process:
|
||||
* 1. Set identity (name, email)
|
||||
* 2. Apply for KYC (IPFS CID, notes)
|
||||
*/
|
||||
export async function submitKycApplication(
|
||||
api: ApiPromise,
|
||||
account: InjectedAccountWithMeta,
|
||||
name: string,
|
||||
email: string,
|
||||
ipfsCid: string,
|
||||
notes: string = 'Citizenship application'
|
||||
): Promise<{ success: boolean; error?: string; blockHash?: string }> {
|
||||
try {
|
||||
if (!api?.tx?.identityKyc?.setIdentity || !api?.tx?.identityKyc?.applyForKyc) {
|
||||
return { success: false, error: 'Identity KYC pallet not available' };
|
||||
}
|
||||
|
||||
// Check if user already has a pending KYC application
|
||||
const pendingApp = await api.query.identityKyc.pendingKycApplications(account.address);
|
||||
if (!pendingApp.isEmpty) {
|
||||
console.log('⚠️ User already has a pending KYC application');
|
||||
return {
|
||||
success: false,
|
||||
error: 'You already have a pending citizenship application. Please wait for approval.'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user is already approved
|
||||
const kycStatus = await api.query.identityKyc.kycStatuses(account.address);
|
||||
if (kycStatus.toString() === 'Approved') {
|
||||
console.log('✅ User KYC is already approved');
|
||||
return {
|
||||
success: false,
|
||||
error: 'Your citizenship application is already approved!'
|
||||
};
|
||||
}
|
||||
|
||||
// Get the injector for signing
|
||||
const injector = await web3FromAddress(account.address);
|
||||
|
||||
// Debug logging
|
||||
console.log('=== submitKycApplication Debug ===');
|
||||
console.log('account.address:', account.address);
|
||||
console.log('name:', name);
|
||||
console.log('email:', email);
|
||||
console.log('ipfsCid:', ipfsCid);
|
||||
console.log('notes:', notes);
|
||||
console.log('===================================');
|
||||
|
||||
// Ensure ipfsCid is a string
|
||||
const cidString = String(ipfsCid);
|
||||
if (!cidString || cidString === 'undefined' || cidString === '[object Object]') {
|
||||
return { success: false, error: `Invalid IPFS CID received: ${cidString}` };
|
||||
}
|
||||
|
||||
// Step 1: Set identity first
|
||||
console.log('Step 1: Setting identity...');
|
||||
const identityResult = await new Promise<{ success: boolean; error?: string }>((resolve, reject) => {
|
||||
api.tx.identityKyc
|
||||
.setIdentity(name, email)
|
||||
.signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError, events }) => {
|
||||
console.log('Identity transaction status:', status.type);
|
||||
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Identity 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('Identity transaction error:', errorMessage);
|
||||
resolve({ success: false, error: errorMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for IdentitySet event
|
||||
const identitySetEvent = events.find(({ event }) =>
|
||||
event.section === 'identityKyc' && event.method === 'IdentitySet'
|
||||
);
|
||||
|
||||
if (identitySetEvent) {
|
||||
console.log('✅ Identity set successfully');
|
||||
resolve({ success: true });
|
||||
} else {
|
||||
resolve({ success: true }); // Still consider it success if in block
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to sign and send identity transaction:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
if (!identityResult.success) {
|
||||
return identityResult;
|
||||
}
|
||||
|
||||
// Step 2: Apply for KYC
|
||||
console.log('Step 2: Applying for KYC...');
|
||||
const result = await new Promise<{ success: boolean; error?: string; blockHash?: string }>((resolve, reject) => {
|
||||
api.tx.identityKyc
|
||||
.applyForKyc([cidString], notes)
|
||||
.signAndSend(account.address, { signer: injector.signer }, ({ 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('Transaction error:', errorMessage);
|
||||
resolve({ success: false, error: errorMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for KycApplied event
|
||||
const kycAppliedEvent = events.find(({ event }) =>
|
||||
event.section === 'identityKyc' && event.method === 'KycApplied'
|
||||
);
|
||||
|
||||
if (kycAppliedEvent) {
|
||||
console.log('✅ KYC Application submitted successfully');
|
||||
resolve({
|
||||
success: true,
|
||||
blockHash: status.asInBlock.toString()
|
||||
});
|
||||
} else {
|
||||
console.warn('Transaction included but KycApplied event not found');
|
||||
resolve({ success: true });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to sign and send transaction:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error('Error submitting KYC application:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Failed to submit KYC application'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to KYC approval events for an address
|
||||
*/
|
||||
export function subscribeToKycApproval(
|
||||
api: ApiPromise,
|
||||
address: string,
|
||||
onApproved: () => void,
|
||||
onError?: (error: string) => void
|
||||
): () => void {
|
||||
try {
|
||||
if (!api?.query?.system?.events) {
|
||||
console.error('Cannot subscribe to events: system.events not available');
|
||||
if (onError) onError('Event subscription not available');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const unsubscribe = api.query.system.events((events) => {
|
||||
events.forEach((record) => {
|
||||
const { event } = record;
|
||||
|
||||
if (event.section === 'identityKyc' && event.method === 'KycApproved') {
|
||||
const [approvedAddress] = event.data;
|
||||
|
||||
if (approvedAddress.toString() === address) {
|
||||
console.log('✅ KYC Approved for:', address);
|
||||
onApproved();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return unsubscribe as unknown as () => void;
|
||||
} catch (error: any) {
|
||||
console.error('Error subscribing to KYC approval:', error);
|
||||
if (onError) onError(error.message || 'Failed to subscribe to approval events');
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FOUNDER ADDRESS
|
||||
// ========================================
|
||||
|
||||
export const FOUNDER_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; // Satoshi Qazi Muhammed
|
||||
@@ -1,130 +0,0 @@
|
||||
// Identity verification types and utilities
|
||||
export interface IdentityProfile {
|
||||
address: string;
|
||||
verificationLevel: 'none' | 'basic' | 'advanced' | 'verified';
|
||||
kycStatus: 'pending' | 'approved' | 'rejected' | 'none';
|
||||
reputationScore: number;
|
||||
badges: Badge[];
|
||||
roles: Role[];
|
||||
verificationDate?: Date;
|
||||
privacySettings: PrivacySettings;
|
||||
}
|
||||
|
||||
export interface Badge {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
earnedDate: Date;
|
||||
category: 'governance' | 'contribution' | 'verification' | 'achievement';
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
assignedDate: Date;
|
||||
expiryDate?: Date;
|
||||
}
|
||||
|
||||
export interface PrivacySettings {
|
||||
showRealName: boolean;
|
||||
showEmail: boolean;
|
||||
showCountry: boolean;
|
||||
useZKProof: boolean;
|
||||
}
|
||||
|
||||
export interface KYCData {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
country?: string;
|
||||
documentType?: 'passport' | 'driver_license' | 'national_id';
|
||||
documentHash?: string;
|
||||
zkProof?: string;
|
||||
}
|
||||
|
||||
export const VERIFICATION_LEVELS = {
|
||||
none: { label: 'Unverified', color: 'gray', minScore: 0 },
|
||||
basic: { label: 'Basic', color: 'blue', minScore: 100 },
|
||||
advanced: { label: 'Advanced', color: 'purple', minScore: 500 },
|
||||
verified: { label: 'Verified', color: 'green', minScore: 1000 }
|
||||
};
|
||||
|
||||
export const DEFAULT_BADGES: Badge[] = [
|
||||
{
|
||||
id: 'early_adopter',
|
||||
name: 'Early Adopter',
|
||||
description: 'Joined during genesis phase',
|
||||
icon: '🚀',
|
||||
color: 'purple',
|
||||
earnedDate: new Date(),
|
||||
category: 'achievement'
|
||||
},
|
||||
{
|
||||
id: 'governance_participant',
|
||||
name: 'Active Voter',
|
||||
description: 'Participated in 10+ proposals',
|
||||
icon: '🗳️',
|
||||
color: 'blue',
|
||||
earnedDate: new Date(),
|
||||
category: 'governance'
|
||||
}
|
||||
];
|
||||
|
||||
export const ROLES = {
|
||||
validator: {
|
||||
id: 'validator',
|
||||
name: 'Validator',
|
||||
permissions: ['validate_blocks', 'propose_blocks', 'vote_proposals']
|
||||
},
|
||||
council_member: {
|
||||
id: 'council_member',
|
||||
name: 'Council Member',
|
||||
permissions: ['create_proposals', 'fast_track_proposals', 'emergency_actions']
|
||||
},
|
||||
verified_user: {
|
||||
id: 'verified_user',
|
||||
name: 'Verified User',
|
||||
permissions: ['vote_proposals', 'create_basic_proposals']
|
||||
}
|
||||
};
|
||||
|
||||
export function calculateReputationScore(
|
||||
activities: any[],
|
||||
verificationLevel: string,
|
||||
badges: Badge[]
|
||||
): number {
|
||||
let score = 0;
|
||||
|
||||
// Base score from verification
|
||||
switch (verificationLevel) {
|
||||
case 'basic': score += 100; break;
|
||||
case 'advanced': score += 500; break;
|
||||
case 'verified': score += 1000; break;
|
||||
}
|
||||
|
||||
// Score from badges
|
||||
score += badges.length * 50;
|
||||
|
||||
// Score from activities (placeholder)
|
||||
score += activities.length * 10;
|
||||
|
||||
return Math.min(score, 2000); // Cap at 2000
|
||||
}
|
||||
|
||||
export function generateZKProof(data: KYCData): string {
|
||||
// Simplified ZK proof generation (in production, use actual ZK library)
|
||||
const hash = btoa(JSON.stringify({
|
||||
...data,
|
||||
timestamp: Date.now(),
|
||||
nonce: Math.random()
|
||||
}));
|
||||
return `zk_${hash.substring(0, 32)}`;
|
||||
}
|
||||
|
||||
export function verifyZKProof(proof: string, expectedData?: any): boolean {
|
||||
// Simplified verification (in production, use actual ZK verification)
|
||||
return proof.startsWith('zk_') && proof.length > 32;
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
// ========================================
|
||||
// Multisig Utilities for USDT Treasury
|
||||
// ========================================
|
||||
// Full on-chain multisig using Substrate pallet-multisig
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import { Tiki } from './tiki';
|
||||
import { encodeAddress, sortAddresses } from '@polkadot/util-crypto';
|
||||
|
||||
// ========================================
|
||||
// MULTISIG CONFIGURATION
|
||||
// ========================================
|
||||
|
||||
export interface MultisigMember {
|
||||
role: string;
|
||||
tiki: Tiki;
|
||||
isUnique: boolean;
|
||||
address?: string; // For non-unique roles, hardcoded address
|
||||
}
|
||||
|
||||
export const USDT_MULTISIG_CONFIG = {
|
||||
threshold: 3,
|
||||
members: [
|
||||
{ role: 'Founder/President', tiki: Tiki.Serok, isUnique: true },
|
||||
{ role: 'Parliament Speaker', tiki: Tiki.SerokiMeclise, isUnique: true },
|
||||
{ role: 'Treasurer', tiki: Tiki.Xezinedar, isUnique: true },
|
||||
{ role: 'Notary', tiki: Tiki.Noter, isUnique: false, address: '' }, // Will be set at runtime
|
||||
{ role: 'Spokesperson', tiki: Tiki.Berdevk, isUnique: false, address: '' },
|
||||
] as MultisigMember[],
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// MULTISIG MEMBER QUERIES
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get all multisig members from on-chain tiki holders
|
||||
* @param api - Polkadot API instance
|
||||
* @param specificAddresses - Addresses for non-unique roles {tiki: address}
|
||||
* @returns Sorted array of member addresses
|
||||
*/
|
||||
export async function getMultisigMembers(
|
||||
api: ApiPromise,
|
||||
specificAddresses: Record<string, string> = {}
|
||||
): Promise<string[]> {
|
||||
const members: string[] = [];
|
||||
|
||||
for (const memberConfig of USDT_MULTISIG_CONFIG.members) {
|
||||
if (memberConfig.isUnique) {
|
||||
// Query from chain for unique roles
|
||||
try {
|
||||
const holder = await api.query.tiki.tikiHolder(memberConfig.tiki);
|
||||
if (holder.isSome) {
|
||||
const address = holder.unwrap().toString();
|
||||
members.push(address);
|
||||
} else {
|
||||
console.warn(`No holder found for unique role: ${memberConfig.tiki}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error querying ${memberConfig.tiki}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Use hardcoded address for non-unique roles
|
||||
const address = specificAddresses[memberConfig.tiki] || memberConfig.address;
|
||||
if (address) {
|
||||
members.push(address);
|
||||
} else {
|
||||
console.warn(`No address specified for non-unique role: ${memberConfig.tiki}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multisig requires sorted addresses
|
||||
return sortAddresses(members);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate deterministic multisig account address
|
||||
* @param members - Sorted array of member addresses
|
||||
* @param threshold - Signature threshold (default: 3)
|
||||
* @param ss58Format - SS58 format for address encoding (default: 42)
|
||||
* @returns Multisig account address
|
||||
*/
|
||||
export function calculateMultisigAddress(
|
||||
members: string[],
|
||||
threshold: number = USDT_MULTISIG_CONFIG.threshold,
|
||||
ss58Format: number = 42
|
||||
): string {
|
||||
// Sort members (multisig requires sorted order)
|
||||
const sortedMembers = sortAddresses(members);
|
||||
|
||||
// Create multisig address
|
||||
// Formula: blake2(b"modlpy/utilisuba" + concat(sorted_members) + threshold)
|
||||
const multisigId = encodeAddress(
|
||||
new Uint8Array([
|
||||
...Buffer.from('modlpy/utilisuba'),
|
||||
...sortedMembers.flatMap((addr) => Array.from(Buffer.from(addr, 'hex'))),
|
||||
threshold,
|
||||
]),
|
||||
ss58Format
|
||||
);
|
||||
|
||||
return multisigId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an address is a multisig member
|
||||
* @param api - Polkadot API instance
|
||||
* @param address - Address to check
|
||||
* @param specificAddresses - Addresses for non-unique roles
|
||||
* @returns boolean
|
||||
*/
|
||||
export async function isMultisigMember(
|
||||
api: ApiPromise,
|
||||
address: string,
|
||||
specificAddresses: Record<string, string> = {}
|
||||
): Promise<boolean> {
|
||||
const members = await getMultisigMembers(api, specificAddresses);
|
||||
return members.includes(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multisig member info for display
|
||||
* @param api - Polkadot API instance
|
||||
* @param specificAddresses - Addresses for non-unique roles
|
||||
* @returns Array of member info objects
|
||||
*/
|
||||
export async function getMultisigMemberInfo(
|
||||
api: ApiPromise,
|
||||
specificAddresses: Record<string, string> = {}
|
||||
): Promise<Array<{ role: string; tiki: Tiki; address: string; isUnique: boolean }>> {
|
||||
const memberInfo = [];
|
||||
|
||||
for (const memberConfig of USDT_MULTISIG_CONFIG.members) {
|
||||
let address = '';
|
||||
|
||||
if (memberConfig.isUnique) {
|
||||
try {
|
||||
const holder = await api.query.tiki.tikiHolder(memberConfig.tiki);
|
||||
if (holder.isSome) {
|
||||
address = holder.unwrap().toString();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error querying ${memberConfig.tiki}:`, error);
|
||||
}
|
||||
} else {
|
||||
address = specificAddresses[memberConfig.tiki] || memberConfig.address || '';
|
||||
}
|
||||
|
||||
if (address) {
|
||||
memberInfo.push({
|
||||
role: memberConfig.role,
|
||||
tiki: memberConfig.tiki,
|
||||
address,
|
||||
isUnique: memberConfig.isUnique,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return memberInfo;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MULTISIG TRANSACTION HELPERS
|
||||
// ========================================
|
||||
|
||||
export interface MultisigTimepoint {
|
||||
height: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new multisig transaction (first signature)
|
||||
* @param api - Polkadot API instance
|
||||
* @param call - The extrinsic to execute via multisig
|
||||
* @param otherSignatories - Other multisig members (excluding current signer)
|
||||
* @param threshold - Signature threshold
|
||||
* @returns Multisig transaction
|
||||
*/
|
||||
export function createMultisigTx(
|
||||
api: ApiPromise,
|
||||
call: SubmittableExtrinsic<'promise'>,
|
||||
otherSignatories: string[],
|
||||
threshold: number = USDT_MULTISIG_CONFIG.threshold
|
||||
) {
|
||||
const maxWeight = {
|
||||
refTime: 1000000000,
|
||||
proofSize: 64 * 1024,
|
||||
};
|
||||
|
||||
return api.tx.multisig.asMulti(
|
||||
threshold,
|
||||
sortAddresses(otherSignatories),
|
||||
null, // No timepoint for first call
|
||||
call,
|
||||
maxWeight
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve an existing multisig transaction
|
||||
* @param api - Polkadot API instance
|
||||
* @param call - The original extrinsic
|
||||
* @param otherSignatories - Other multisig members
|
||||
* @param timepoint - Block height and index of the first approval
|
||||
* @param threshold - Signature threshold
|
||||
* @returns Approval transaction
|
||||
*/
|
||||
export function approveMultisigTx(
|
||||
api: ApiPromise,
|
||||
call: SubmittableExtrinsic<'promise'>,
|
||||
otherSignatories: string[],
|
||||
timepoint: MultisigTimepoint,
|
||||
threshold: number = USDT_MULTISIG_CONFIG.threshold
|
||||
) {
|
||||
const maxWeight = {
|
||||
refTime: 1000000000,
|
||||
proofSize: 64 * 1024,
|
||||
};
|
||||
|
||||
return api.tx.multisig.asMulti(
|
||||
threshold,
|
||||
sortAddresses(otherSignatories),
|
||||
timepoint,
|
||||
call,
|
||||
maxWeight
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a multisig transaction
|
||||
* @param api - Polkadot API instance
|
||||
* @param callHash - Hash of the call to cancel
|
||||
* @param otherSignatories - Other multisig members
|
||||
* @param timepoint - Block height and index of the call
|
||||
* @param threshold - Signature threshold
|
||||
* @returns Cancel transaction
|
||||
*/
|
||||
export function cancelMultisigTx(
|
||||
api: ApiPromise,
|
||||
callHash: string,
|
||||
otherSignatories: string[],
|
||||
timepoint: MultisigTimepoint,
|
||||
threshold: number = USDT_MULTISIG_CONFIG.threshold
|
||||
) {
|
||||
return api.tx.multisig.cancelAsMulti(
|
||||
threshold,
|
||||
sortAddresses(otherSignatories),
|
||||
timepoint,
|
||||
callHash
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MULTISIG STORAGE QUERIES
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get pending multisig calls
|
||||
* @param api - Polkadot API instance
|
||||
* @param multisigAddress - The multisig account address
|
||||
* @returns Array of pending calls
|
||||
*/
|
||||
export async function getPendingMultisigCalls(
|
||||
api: ApiPromise,
|
||||
multisigAddress: string
|
||||
): Promise<any[]> {
|
||||
try {
|
||||
const multisigs = await api.query.multisig.multisigs.entries(multisigAddress);
|
||||
|
||||
return multisigs.map(([key, value]) => {
|
||||
const callHash = key.args[1].toHex();
|
||||
const multisigData = value.toJSON() as any;
|
||||
|
||||
return {
|
||||
callHash,
|
||||
when: multisigData.when,
|
||||
deposit: multisigData.deposit,
|
||||
depositor: multisigData.depositor,
|
||||
approvals: multisigData.approvals,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching pending multisig calls:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DISPLAY HELPERS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Format multisig address for display
|
||||
* @param address - Full multisig address
|
||||
* @returns Shortened address
|
||||
*/
|
||||
export function formatMultisigAddress(address: string): string {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 8)}...${address.slice(-8)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approval status text
|
||||
* @param approvals - Number of approvals
|
||||
* @param threshold - Required threshold
|
||||
* @returns Status text
|
||||
*/
|
||||
export function getApprovalStatus(approvals: number, threshold: number): string {
|
||||
if (approvals >= threshold) return 'Ready to Execute';
|
||||
return `${approvals}/${threshold} Approvals`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approval status color
|
||||
* @param approvals - Number of approvals
|
||||
* @param threshold - Required threshold
|
||||
* @returns Tailwind color class
|
||||
*/
|
||||
export function getApprovalStatusColor(approvals: number, threshold: number): string {
|
||||
if (approvals >= threshold) return 'text-green-500';
|
||||
if (approvals >= threshold - 1) return 'text-yellow-500';
|
||||
return 'text-gray-500';
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
// ========================================
|
||||
// Score Systems Integration
|
||||
// ========================================
|
||||
// Centralized score fetching from blockchain pallets
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
|
||||
// ========================================
|
||||
// TYPE DEFINITIONS
|
||||
// ========================================
|
||||
|
||||
export interface UserScores {
|
||||
trustScore: number;
|
||||
referralScore: number;
|
||||
stakingScore: number;
|
||||
tikiScore: number;
|
||||
totalScore: number;
|
||||
}
|
||||
|
||||
export interface TrustScoreDetails {
|
||||
totalScore: number;
|
||||
stakingPoints: number;
|
||||
referralPoints: number;
|
||||
tikiPoints: number;
|
||||
activityPoints: number;
|
||||
historyLength: number;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TRUST SCORE (pallet_trust)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Fetch user's trust score from blockchain
|
||||
* pallet_trust::TrustScores storage
|
||||
*/
|
||||
export async function getTrustScore(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
if (!api?.query?.trust) {
|
||||
console.warn('Trust pallet not available');
|
||||
return 0;
|
||||
}
|
||||
|
||||
const score = await api.query.trust.trustScores(address);
|
||||
|
||||
if (score.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(score.toString());
|
||||
} catch (error) {
|
||||
console.error('Error fetching trust score:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch detailed trust score breakdown
|
||||
* pallet_trust::ScoreHistory storage
|
||||
*/
|
||||
export async function getTrustScoreDetails(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<TrustScoreDetails | null> {
|
||||
try {
|
||||
if (!api?.query?.trust) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalScore = await getTrustScore(api, address);
|
||||
|
||||
// Get score history to show detailed breakdown
|
||||
const historyResult = await api.query.trust.scoreHistory(address);
|
||||
|
||||
if (historyResult.isEmpty) {
|
||||
return {
|
||||
totalScore,
|
||||
stakingPoints: 0,
|
||||
referralPoints: 0,
|
||||
tikiPoints: 0,
|
||||
activityPoints: 0,
|
||||
historyLength: 0
|
||||
};
|
||||
}
|
||||
|
||||
const history = historyResult.toJSON() as any[];
|
||||
|
||||
// Calculate points from history
|
||||
// History format: [{blockNumber, score, reason}]
|
||||
let stakingPoints = 0;
|
||||
let referralPoints = 0;
|
||||
let tikiPoints = 0;
|
||||
let activityPoints = 0;
|
||||
|
||||
for (const entry of history) {
|
||||
const reason = entry.reason || '';
|
||||
const score = entry.score || 0;
|
||||
|
||||
if (reason.includes('Staking')) stakingPoints += score;
|
||||
else if (reason.includes('Referral')) referralPoints += score;
|
||||
else if (reason.includes('Tiki') || reason.includes('Role')) tikiPoints += score;
|
||||
else activityPoints += score;
|
||||
}
|
||||
|
||||
return {
|
||||
totalScore,
|
||||
stakingPoints,
|
||||
referralPoints,
|
||||
tikiPoints,
|
||||
activityPoints,
|
||||
historyLength: history.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching trust score details:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// REFERRAL SCORE (pallet_trust)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Fetch user's referral score
|
||||
* pallet_trust::ReferralScores storage
|
||||
*/
|
||||
export async function getReferralScore(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
if (!api?.query?.trust?.referralScores) {
|
||||
console.warn('Referral scores not available in trust pallet');
|
||||
return 0;
|
||||
}
|
||||
|
||||
const score = await api.query.trust.referralScores(address);
|
||||
|
||||
if (score.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(score.toString());
|
||||
} catch (error) {
|
||||
console.error('Error fetching referral score:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get referral count for user
|
||||
* pallet_trust::Referrals storage
|
||||
*/
|
||||
export async function getReferralCount(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
if (!api?.query?.trust?.referrals) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const referrals = await api.query.trust.referrals(address);
|
||||
|
||||
if (referrals.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const referralList = referrals.toJSON() as any[];
|
||||
return Array.isArray(referralList) ? referralList.length : 0;
|
||||
} catch (error) {
|
||||
console.error('Error fetching referral count:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STAKING SCORE (pallet_staking_score)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get staking score from pallet_staking_score
|
||||
* This is already implemented in lib/staking.ts
|
||||
* Re-exported here for consistency
|
||||
*/
|
||||
export async function getStakingScoreFromPallet(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
if (!api?.query?.stakingScore) {
|
||||
console.warn('Staking score pallet not available');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if user has started score tracking
|
||||
const scoreResult = await api.query.stakingScore.stakingStartBlock(address);
|
||||
|
||||
if (scoreResult.isNone) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get staking info from staking pallet
|
||||
const ledger = await api.query.staking.ledger(address);
|
||||
|
||||
if (ledger.isNone) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ledgerData = ledger.unwrap().toJSON() as any;
|
||||
const stakedAmount = Number(ledgerData.total || 0) / 1e12; // Convert to HEZ
|
||||
|
||||
// Get duration
|
||||
const startBlock = Number(scoreResult.unwrap().toString());
|
||||
const currentBlock = Number((await api.query.system.number()).toString());
|
||||
const durationInBlocks = currentBlock - startBlock;
|
||||
|
||||
// Calculate score based on amount and duration
|
||||
// Amount-based score (20-50 points)
|
||||
let amountScore = 20;
|
||||
if (stakedAmount <= 100) amountScore = 20;
|
||||
else if (stakedAmount <= 250) amountScore = 30;
|
||||
else if (stakedAmount <= 750) amountScore = 40;
|
||||
else amountScore = 50;
|
||||
|
||||
// Duration multiplier
|
||||
const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // ~30 days
|
||||
let durationMultiplier = 1.0;
|
||||
|
||||
if (durationInBlocks >= 12 * MONTH_IN_BLOCKS) durationMultiplier = 2.0;
|
||||
else if (durationInBlocks >= 6 * MONTH_IN_BLOCKS) durationMultiplier = 1.7;
|
||||
else if (durationInBlocks >= 3 * MONTH_IN_BLOCKS) durationMultiplier = 1.4;
|
||||
else if (durationInBlocks >= MONTH_IN_BLOCKS) durationMultiplier = 1.2;
|
||||
|
||||
return Math.min(100, Math.floor(amountScore * durationMultiplier));
|
||||
} catch (error) {
|
||||
console.error('Error fetching staking score:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TIKI SCORE (from lib/tiki.ts)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Calculate Tiki score from user's roles
|
||||
* Import from lib/tiki.ts
|
||||
*/
|
||||
import { fetchUserTikis, calculateTikiScore } from './tiki';
|
||||
|
||||
export async function getTikiScore(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<number> {
|
||||
try {
|
||||
const tikis = await fetchUserTikis(api, address);
|
||||
return calculateTikiScore(tikis);
|
||||
} catch (error) {
|
||||
console.error('Error fetching tiki score:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COMPREHENSIVE SCORE FETCHING
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Fetch all scores for a user in one call
|
||||
*/
|
||||
export async function getAllScores(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<UserScores> {
|
||||
try {
|
||||
if (!api || !address) {
|
||||
return {
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch all scores in parallel
|
||||
const [trustScore, referralScore, stakingScore, tikiScore] = await Promise.all([
|
||||
getTrustScore(api, address),
|
||||
getReferralScore(api, address),
|
||||
getStakingScoreFromPallet(api, address),
|
||||
getTikiScore(api, address)
|
||||
]);
|
||||
|
||||
const totalScore = trustScore + referralScore + stakingScore + tikiScore;
|
||||
|
||||
return {
|
||||
trustScore,
|
||||
referralScore,
|
||||
stakingScore,
|
||||
tikiScore,
|
||||
totalScore
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching all scores:', error);
|
||||
return {
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SCORE DISPLAY HELPERS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get color class based on score
|
||||
*/
|
||||
export function getScoreColor(score: number): string {
|
||||
if (score >= 200) return 'text-purple-500';
|
||||
if (score >= 150) return 'text-pink-500';
|
||||
if (score >= 100) return 'text-blue-500';
|
||||
if (score >= 70) return 'text-cyan-500';
|
||||
if (score >= 40) return 'text-teal-500';
|
||||
if (score >= 20) return 'text-green-500';
|
||||
return 'text-gray-500';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get score rating label
|
||||
*/
|
||||
export function getScoreRating(score: number): string {
|
||||
if (score >= 250) return 'Legendary';
|
||||
if (score >= 200) return 'Excellent';
|
||||
if (score >= 150) return 'Very Good';
|
||||
if (score >= 100) return 'Good';
|
||||
if (score >= 70) return 'Average';
|
||||
if (score >= 40) return 'Fair';
|
||||
if (score >= 20) return 'Low';
|
||||
return 'Very Low';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format score for display
|
||||
*/
|
||||
export function formatScore(score: number): string {
|
||||
return score.toFixed(0);
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
// ========================================
|
||||
// Staking Helper Functions
|
||||
// ========================================
|
||||
// Helper functions for pallet_staking and pallet_staking_score integration
|
||||
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
import { formatBalance } from './wallet';
|
||||
|
||||
export interface StakingLedger {
|
||||
stash: string;
|
||||
total: string;
|
||||
active: string;
|
||||
unlocking: { value: string; era: number }[];
|
||||
claimedRewards: number[];
|
||||
}
|
||||
|
||||
export interface NominatorInfo {
|
||||
targets: string[];
|
||||
submittedIn: number;
|
||||
suppressed: boolean;
|
||||
}
|
||||
|
||||
export interface ValidatorPrefs {
|
||||
commission: number;
|
||||
blocked: boolean;
|
||||
}
|
||||
|
||||
export interface EraRewardPoints {
|
||||
total: number;
|
||||
individual: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface PezRewardInfo {
|
||||
currentEpoch: number;
|
||||
epochStartBlock: number;
|
||||
claimableRewards: { epoch: number; amount: string }[]; // Unclaimed rewards from completed epochs
|
||||
totalClaimable: string;
|
||||
hasPendingClaim: boolean;
|
||||
}
|
||||
|
||||
export interface StakingInfo {
|
||||
bonded: string;
|
||||
active: string;
|
||||
unlocking: { amount: string; era: number; blocksRemaining: number }[];
|
||||
redeemable: string;
|
||||
nominations: string[];
|
||||
stakingScore: number | null;
|
||||
stakingDuration: number | null; // Duration in blocks
|
||||
hasStartedScoreTracking: boolean;
|
||||
isValidator: boolean;
|
||||
pezRewards: PezRewardInfo | null; // PEZ rewards information
|
||||
}
|
||||
|
||||
/**
|
||||
* Get staking ledger for an account
|
||||
* In Substrate staking, we need to query using the controller account.
|
||||
* If stash == controller (modern setup), we can query directly.
|
||||
*/
|
||||
export async function getStakingLedger(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<StakingLedger | null> {
|
||||
try {
|
||||
// Method 1: Try direct ledger query (modern Substrate where stash == controller)
|
||||
let ledgerResult = await api.query.staking.ledger(address);
|
||||
|
||||
// Method 2: If not found, check if address is a stash and get controller
|
||||
if (ledgerResult.isNone) {
|
||||
const bondedController = await api.query.staking.bonded(address);
|
||||
if (bondedController.isSome) {
|
||||
const controllerAddress = bondedController.unwrap().toString();
|
||||
console.log(`Found controller ${controllerAddress} for stash ${address}`);
|
||||
ledgerResult = await api.query.staking.ledger(controllerAddress);
|
||||
}
|
||||
}
|
||||
|
||||
if (ledgerResult.isNone) {
|
||||
console.warn(`No staking ledger found for ${address}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const ledger = ledgerResult.unwrap();
|
||||
const ledgerJson = ledger.toJSON() as any;
|
||||
|
||||
console.log('Staking ledger:', ledgerJson);
|
||||
|
||||
return {
|
||||
stash: ledgerJson.stash?.toString() || address,
|
||||
total: ledgerJson.total?.toString() || '0',
|
||||
active: ledgerJson.active?.toString() || '0',
|
||||
unlocking: (ledgerJson.unlocking || []).map((u: any) => ({
|
||||
value: u.value?.toString() || '0',
|
||||
era: u.era || 0
|
||||
})),
|
||||
claimedRewards: ledgerJson.claimedRewards || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching staking ledger:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nominations for an account
|
||||
*/
|
||||
export async function getNominations(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<NominatorInfo | null> {
|
||||
try {
|
||||
const nominatorsOption = await api.query.staking.nominators(address);
|
||||
|
||||
if (nominatorsOption.isNone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nominator = nominatorsOption.unwrap();
|
||||
const nominatorJson = nominator.toJSON() as any;
|
||||
|
||||
return {
|
||||
targets: nominatorJson.targets || [],
|
||||
submittedIn: nominatorJson.submittedIn || 0,
|
||||
suppressed: nominatorJson.suppressed || false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching nominations:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current active era
|
||||
*/
|
||||
export async function getCurrentEra(api: ApiPromise): Promise<number> {
|
||||
try {
|
||||
const activeEraOption = await api.query.staking.activeEra();
|
||||
if (activeEraOption.isNone) {
|
||||
return 0;
|
||||
}
|
||||
const activeEra = activeEraOption.unwrap();
|
||||
return Number(activeEra.index.toString());
|
||||
} catch (error) {
|
||||
console.error('Error fetching current era:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocks remaining until an era
|
||||
*/
|
||||
export async function getBlocksUntilEra(
|
||||
api: ApiPromise,
|
||||
targetEra: number
|
||||
): Promise<number> {
|
||||
try {
|
||||
const currentEra = await getCurrentEra(api);
|
||||
if (targetEra <= currentEra) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const activeEraOption = await api.query.staking.activeEra();
|
||||
if (activeEraOption.isNone) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const activeEra = activeEraOption.unwrap();
|
||||
const eraStartBlock = Number(activeEra.start.unwrapOr(0).toString());
|
||||
|
||||
// Get session length and sessions per era
|
||||
const sessionLength = api.consts.babe?.epochDuration || api.consts.timestamp?.minimumPeriod || 600;
|
||||
const sessionsPerEra = api.consts.staking.sessionsPerEra;
|
||||
|
||||
const blocksPerEra = Number(sessionLength.toString()) * Number(sessionsPerEra.toString());
|
||||
const currentBlock = Number((await api.query.system.number()).toString());
|
||||
|
||||
const erasRemaining = targetEra - currentEra;
|
||||
const blocksIntoCurrentEra = currentBlock - eraStartBlock;
|
||||
const blocksRemainingInCurrentEra = blocksPerEra - blocksIntoCurrentEra;
|
||||
|
||||
return blocksRemainingInCurrentEra + (blocksPerEra * (erasRemaining - 1));
|
||||
} catch (error) {
|
||||
console.error('Error calculating blocks until era:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PEZ rewards information for an account
|
||||
*/
|
||||
export async function getPezRewards(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<PezRewardInfo | null> {
|
||||
try {
|
||||
// Check if pezRewards pallet exists
|
||||
if (!api.query.pezRewards || !api.query.pezRewards.epochInfo) {
|
||||
console.warn('PezRewards pallet not available');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get current epoch info
|
||||
const epochInfoResult = await api.query.pezRewards.epochInfo();
|
||||
|
||||
if (!epochInfoResult) {
|
||||
console.warn('No epoch info found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const epochInfo = epochInfoResult.toJSON() as any;
|
||||
const currentEpoch = epochInfo.currentEpoch || 0;
|
||||
const epochStartBlock = epochInfo.epochStartBlock || 0;
|
||||
|
||||
// Check for claimable rewards from completed epochs
|
||||
const claimableRewards: { epoch: number; amount: string }[] = [];
|
||||
let totalClaimable = BigInt(0);
|
||||
|
||||
// Check last 3 completed epochs for unclaimed rewards
|
||||
for (let i = Math.max(0, currentEpoch - 3); i < currentEpoch; i++) {
|
||||
try {
|
||||
// Check if user has claimed this epoch already
|
||||
const claimedResult = await api.query.pezRewards.claimedRewards(i, address);
|
||||
|
||||
if (claimedResult.isNone) {
|
||||
// User hasn't claimed - check if they have rewards
|
||||
const userScoreResult = await api.query.pezRewards.userEpochScores(i, address);
|
||||
|
||||
if (userScoreResult.isSome) {
|
||||
// User has a score for this epoch - calculate their reward
|
||||
const epochPoolResult = await api.query.pezRewards.epochRewardPools(i);
|
||||
|
||||
if (epochPoolResult.isSome) {
|
||||
const epochPool = epochPoolResult.unwrap().toJSON() as any;
|
||||
const userScore = BigInt(userScoreResult.unwrap().toString());
|
||||
const rewardPerPoint = BigInt(epochPool.rewardPerTrustPoint || '0');
|
||||
|
||||
const rewardAmount = userScore * rewardPerPoint;
|
||||
const rewardFormatted = formatBalance(rewardAmount.toString());
|
||||
|
||||
if (parseFloat(rewardFormatted) > 0) {
|
||||
claimableRewards.push({
|
||||
epoch: i,
|
||||
amount: rewardFormatted
|
||||
});
|
||||
totalClaimable += rewardAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Error checking epoch ${i} rewards:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentEpoch,
|
||||
epochStartBlock,
|
||||
claimableRewards,
|
||||
totalClaimable: formatBalance(totalClaimable.toString()),
|
||||
hasPendingClaim: claimableRewards.length > 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('PEZ rewards not available:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive staking info for an account
|
||||
*/
|
||||
export async function getStakingInfo(
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<StakingInfo> {
|
||||
const ledger = await getStakingLedger(api, address);
|
||||
const nominations = await getNominations(api, address);
|
||||
const currentEra = await getCurrentEra(api);
|
||||
|
||||
const unlocking = ledger?.unlocking || [];
|
||||
const unlockingWithBlocks = await Promise.all(
|
||||
unlocking.map(async (u) => {
|
||||
const blocks = await getBlocksUntilEra(api, u.era);
|
||||
return {
|
||||
amount: formatBalance(u.value),
|
||||
era: u.era,
|
||||
blocksRemaining: blocks
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Calculate redeemable (unlocking chunks where era has passed)
|
||||
const redeemableChunks = unlocking.filter(u => u.era <= currentEra);
|
||||
const redeemable = redeemableChunks.reduce((sum, u) => {
|
||||
return sum + BigInt(u.value);
|
||||
}, BigInt(0));
|
||||
|
||||
// Get staking score if available
|
||||
//
|
||||
// IMPORTANT: This calculation MUST match pallet_staking_score::get_staking_score() exactly!
|
||||
// The pallet calculates this score and reports it to pallet_pez_rewards.
|
||||
// Any changes here must be synchronized with pallets/staking-score/src/lib.rs
|
||||
//
|
||||
// Score Formula:
|
||||
// 1. Amount Score (20-50 points based on staked HEZ)
|
||||
// - 0-100 HEZ: 20 points
|
||||
// - 101-250 HEZ: 30 points
|
||||
// - 251-750 HEZ: 40 points
|
||||
// - 751+ HEZ: 50 points
|
||||
// 2. Duration Multiplier (based on time staked)
|
||||
// - < 1 month: x1.0
|
||||
// - 1-2 months: x1.2
|
||||
// - 3-5 months: x1.4
|
||||
// - 6-11 months: x1.7
|
||||
// - 12+ months: x2.0
|
||||
// 3. Final Score = min(100, floor(amountScore * durationMultiplier))
|
||||
//
|
||||
let stakingScore: number | null = null;
|
||||
let stakingDuration: number | null = null;
|
||||
let hasStartedScoreTracking = false;
|
||||
|
||||
try {
|
||||
if (api.query.stakingScore && api.query.stakingScore.stakingStartBlock) {
|
||||
// Check if user has started score tracking
|
||||
const scoreResult = await api.query.stakingScore.stakingStartBlock(address);
|
||||
|
||||
if (scoreResult.isSome) {
|
||||
hasStartedScoreTracking = true;
|
||||
const startBlock = Number(scoreResult.unwrap().toString());
|
||||
const currentBlock = Number((await api.query.system.number()).toString());
|
||||
const durationInBlocks = currentBlock - startBlock;
|
||||
stakingDuration = durationInBlocks;
|
||||
|
||||
// Calculate amount-based score (20-50 points)
|
||||
const stakedHEZ = ledger ? parseFloat(formatBalance(ledger.total)) : 0;
|
||||
let amountScore = 20; // Default
|
||||
|
||||
if (stakedHEZ <= 100) {
|
||||
amountScore = 20;
|
||||
} else if (stakedHEZ <= 250) {
|
||||
amountScore = 30;
|
||||
} else if (stakedHEZ <= 750) {
|
||||
amountScore = 40;
|
||||
} else {
|
||||
amountScore = 50; // 751+ HEZ
|
||||
}
|
||||
|
||||
// Calculate duration multiplier
|
||||
const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // 432,000 blocks (~30 days, 6s per block)
|
||||
let durationMultiplier = 1.0;
|
||||
|
||||
if (durationInBlocks >= 12 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 2.0; // 12+ months
|
||||
} else if (durationInBlocks >= 6 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.7; // 6-11 months
|
||||
} else if (durationInBlocks >= 3 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.4; // 3-5 months
|
||||
} else if (durationInBlocks >= MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.2; // 1-2 months
|
||||
} else {
|
||||
durationMultiplier = 1.0; // < 1 month
|
||||
}
|
||||
|
||||
// Final score calculation (max 100)
|
||||
// This MUST match the pallet's integer math: amount_score * multiplier_numerator / multiplier_denominator
|
||||
stakingScore = Math.min(100, Math.floor(amountScore * durationMultiplier));
|
||||
|
||||
console.log('Staking score calculated:', {
|
||||
stakedHEZ,
|
||||
amountScore,
|
||||
durationInBlocks,
|
||||
durationMultiplier,
|
||||
finalScore: stakingScore
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Staking score not available:', error);
|
||||
}
|
||||
|
||||
// Check if validator
|
||||
const validatorsOption = await api.query.staking.validators(address);
|
||||
const isValidator = validatorsOption.isSome;
|
||||
|
||||
// Get PEZ rewards information
|
||||
const pezRewards = await getPezRewards(api, address);
|
||||
|
||||
return {
|
||||
bonded: ledger ? formatBalance(ledger.total) : '0',
|
||||
active: ledger ? formatBalance(ledger.active) : '0',
|
||||
unlocking: unlockingWithBlocks,
|
||||
redeemable: formatBalance(redeemable.toString()),
|
||||
nominations: nominations?.targets || [],
|
||||
stakingScore,
|
||||
stakingDuration,
|
||||
hasStartedScoreTracking,
|
||||
isValidator,
|
||||
pezRewards
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of active validators
|
||||
* For Pezkuwi, we query staking.validators.entries() to get all registered validators
|
||||
*/
|
||||
export async function getActiveValidators(api: ApiPromise): Promise<string[]> {
|
||||
try {
|
||||
// Try multiple methods to get validators
|
||||
|
||||
// Method 1: Try validatorPool.currentValidatorSet() if available
|
||||
if (api.query.validatorPool && api.query.validatorPool.currentValidatorSet) {
|
||||
try {
|
||||
const currentSetOption = await api.query.validatorPool.currentValidatorSet();
|
||||
if (currentSetOption.isSome) {
|
||||
const validatorSet = currentSetOption.unwrap() as any;
|
||||
// Extract validators array from the set structure
|
||||
if (validatorSet.validators && Array.isArray(validatorSet.validators)) {
|
||||
const validators = validatorSet.validators.map((v: any) => v.toString());
|
||||
if (validators.length > 0) {
|
||||
console.log(`Found ${validators.length} validators from validatorPool.currentValidatorSet`);
|
||||
return validators;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('validatorPool.currentValidatorSet query failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Query staking.validators.entries() to get all registered validators
|
||||
try {
|
||||
const validatorEntries = await api.query.staking.validators.entries();
|
||||
if (validatorEntries.length > 0) {
|
||||
const validators = validatorEntries.map(([key]) => key.args[0].toString());
|
||||
console.log(`Found ${validators.length} validators from staking.validators.entries()`);
|
||||
return validators;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('staking.validators.entries() query failed:', err);
|
||||
}
|
||||
|
||||
// Method 3: Fallback to session.validators()
|
||||
const sessionValidators = await api.query.session.validators();
|
||||
const validators = sessionValidators.map(v => v.toString());
|
||||
console.log(`Found ${validators.length} validators from session.validators()`);
|
||||
return validators;
|
||||
} catch (error) {
|
||||
console.error('Error fetching validators:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum nominator bond
|
||||
*/
|
||||
export async function getMinNominatorBond(api: ApiPromise): Promise<string> {
|
||||
try {
|
||||
const minBond = await api.query.staking.minNominatorBond();
|
||||
return formatBalance(minBond.toString());
|
||||
} catch (error) {
|
||||
console.error('Error fetching min nominator bond:', error);
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bonding duration in eras
|
||||
*/
|
||||
export async function getBondingDuration(api: ApiPromise): Promise<number> {
|
||||
try {
|
||||
const duration = api.consts.staking.bondingDuration;
|
||||
return Number(duration.toString());
|
||||
} catch (error) {
|
||||
console.error('Error fetching bonding duration:', error);
|
||||
return 28; // Default 28 eras
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse amount to blockchain format (12 decimals for HEZ)
|
||||
*/
|
||||
export function parseAmount(amount: string | number, decimals: number = 12): string {
|
||||
const amountNum = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
if (isNaN(amountNum) || amountNum <= 0) {
|
||||
throw new Error('Invalid amount');
|
||||
}
|
||||
const value = BigInt(Math.floor(amountNum * Math.pow(10, decimals)));
|
||||
return value.toString();
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
// ========================================
|
||||
// Pallet-Tiki Integration
|
||||
// ========================================
|
||||
// This file handles all tiki-related blockchain interactions
|
||||
// Based on: /Pezkuwi-SDK/pezkuwi/pallets/tiki/src/lib.rs
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
|
||||
// ========================================
|
||||
// TIKI TYPES (from Rust enum)
|
||||
// ========================================
|
||||
export enum Tiki {
|
||||
// Otomatik - KYC sonrası
|
||||
Hemwelatî = 'Hemwelatî',
|
||||
|
||||
// Seçilen roller (Elected)
|
||||
Parlementer = 'Parlementer',
|
||||
SerokiMeclise = 'SerokiMeclise',
|
||||
Serok = 'Serok',
|
||||
|
||||
// Atanan roller (Appointed) - Yargı
|
||||
EndameDiwane = 'EndameDiwane',
|
||||
Dadger = 'Dadger',
|
||||
Dozger = 'Dozger',
|
||||
Hiquqnas = 'Hiquqnas',
|
||||
Noter = 'Noter',
|
||||
|
||||
// Atanan roller - Yürütme
|
||||
Wezir = 'Wezir',
|
||||
SerokWeziran = 'SerokWeziran',
|
||||
WezireDarayiye = 'WezireDarayiye',
|
||||
WezireParez = 'WezireParez',
|
||||
WezireDad = 'WezireDad',
|
||||
WezireBelaw = 'WezireBelaw',
|
||||
WezireTend = 'WezireTend',
|
||||
WezireAva = 'WezireAva',
|
||||
WezireCand = 'WezireCand',
|
||||
|
||||
// Atanan roller - İdari
|
||||
Xezinedar = 'Xezinedar',
|
||||
Bacgir = 'Bacgir',
|
||||
GerinendeyeCavkaniye = 'GerinendeyeCavkaniye',
|
||||
OperatorêTorê = 'OperatorêTorê',
|
||||
PisporêEwlehiyaSîber = 'PisporêEwlehiyaSîber',
|
||||
GerinendeyeDaneye = 'GerinendeyeDaneye',
|
||||
Berdevk = 'Berdevk',
|
||||
Qeydkar = 'Qeydkar',
|
||||
Balyoz = 'Balyoz',
|
||||
Navbeynkar = 'Navbeynkar',
|
||||
ParêzvaneÇandî = 'ParêzvaneÇandî',
|
||||
Mufetîs = 'Mufetîs',
|
||||
KalîteKontrolker = 'KalîteKontrolker',
|
||||
|
||||
// Atanan roller - Kültürel/Dini
|
||||
Mela = 'Mela',
|
||||
Feqî = 'Feqî',
|
||||
Perwerdekar = 'Perwerdekar',
|
||||
Rewsenbîr = 'Rewsenbîr',
|
||||
RêveberêProjeyê = 'RêveberêProjeyê',
|
||||
SerokêKomele = 'SerokêKomele',
|
||||
ModeratorêCivakê = 'ModeratorêCivakê',
|
||||
|
||||
// Kazanılan roller (Earned)
|
||||
Axa = 'Axa',
|
||||
Pêseng = 'Pêseng',
|
||||
Sêwirmend = 'Sêwirmend',
|
||||
Hekem = 'Hekem',
|
||||
Mamoste = 'Mamoste',
|
||||
|
||||
// Ekonomik rol
|
||||
Bazargan = 'Bazargan',
|
||||
}
|
||||
|
||||
// Role assignment types
|
||||
export enum RoleAssignmentType {
|
||||
Automatic = 'Automatic',
|
||||
Appointed = 'Appointed',
|
||||
Elected = 'Elected',
|
||||
Earned = 'Earned',
|
||||
}
|
||||
|
||||
// Tiki to Display Name mapping (English)
|
||||
export const TIKI_DISPLAY_NAMES: Record<string, string> = {
|
||||
Hemwelatî: 'Citizen',
|
||||
Parlementer: 'Parliament Member',
|
||||
SerokiMeclise: 'Speaker of Parliament',
|
||||
Serok: 'President',
|
||||
Wezir: 'Minister',
|
||||
SerokWeziran: 'Prime Minister',
|
||||
WezireDarayiye: 'Minister of Finance',
|
||||
WezireParez: 'Minister of Defense',
|
||||
WezireDad: 'Minister of Justice',
|
||||
WezireBelaw: 'Minister of Education',
|
||||
WezireTend: 'Minister of Health',
|
||||
WezireAva: 'Minister of Water',
|
||||
WezireCand: 'Minister of Culture',
|
||||
EndameDiwane: 'Supreme Court Member',
|
||||
Dadger: 'Judge',
|
||||
Dozger: 'Prosecutor',
|
||||
Hiquqnas: 'Lawyer',
|
||||
Noter: 'Notary',
|
||||
Xezinedar: 'Treasurer',
|
||||
Bacgir: 'Tax Collector',
|
||||
GerinendeyeCavkaniye: 'Resource Manager',
|
||||
OperatorêTorê: 'Network Operator',
|
||||
PisporêEwlehiyaSîber: 'Cybersecurity Expert',
|
||||
GerinendeyeDaneye: 'Data Manager',
|
||||
Berdevk: 'Representative',
|
||||
Qeydkar: 'Registrar',
|
||||
Balyoz: 'Ambassador',
|
||||
Navbeynkar: 'Mediator',
|
||||
ParêzvaneÇandî: 'Cultural Guardian',
|
||||
Mufetîs: 'Inspector',
|
||||
KalîteKontrolker: 'Quality Controller',
|
||||
Mela: 'Religious Scholar',
|
||||
Feqî: 'Religious Jurist',
|
||||
Perwerdekar: 'Educator',
|
||||
Rewsenbîr: 'Intellectual',
|
||||
RêveberêProjeyê: 'Project Manager',
|
||||
SerokêKomele: 'Community Leader',
|
||||
ModeratorêCivakê: 'Community Moderator',
|
||||
Axa: 'Elder',
|
||||
Pêseng: 'Pioneer',
|
||||
Sêwirmend: 'Advisor',
|
||||
Hekem: 'Expert',
|
||||
Mamoste: 'Teacher',
|
||||
Bazargan: 'Merchant',
|
||||
};
|
||||
|
||||
// Tiki scores (from get_bonus_for_tiki function)
|
||||
export const TIKI_SCORES: Record<string, number> = {
|
||||
Axa: 250,
|
||||
RêveberêProjeyê: 250,
|
||||
ModeratorêCivakê: 200,
|
||||
Serok: 200,
|
||||
EndameDiwane: 175,
|
||||
Dadger: 150,
|
||||
SerokiMeclise: 150,
|
||||
SerokWeziran: 125,
|
||||
Dozger: 120,
|
||||
Wezir: 100,
|
||||
WezireDarayiye: 100,
|
||||
WezireParez: 100,
|
||||
WezireDad: 100,
|
||||
WezireBelaw: 100,
|
||||
WezireTend: 100,
|
||||
WezireAva: 100,
|
||||
WezireCand: 100,
|
||||
SerokêKomele: 100,
|
||||
Xezinedar: 100,
|
||||
PisporêEwlehiyaSîber: 100,
|
||||
Parlementer: 100,
|
||||
Mufetîs: 90,
|
||||
Balyoz: 80,
|
||||
Hiquqnas: 75,
|
||||
Berdevk: 70,
|
||||
Mamoste: 70,
|
||||
Bazargan: 60,
|
||||
OperatorêTorê: 60,
|
||||
Mela: 50,
|
||||
Feqî: 50,
|
||||
Noter: 50,
|
||||
Bacgir: 50,
|
||||
Perwerdekar: 40,
|
||||
Rewsenbîr: 40,
|
||||
GerinendeyeCavkaniye: 40,
|
||||
GerinendeyeDaneye: 40,
|
||||
KalîteKontrolker: 30,
|
||||
Navbeynkar: 30,
|
||||
Hekem: 30,
|
||||
Qeydkar: 25,
|
||||
ParêzvaneÇandî: 25,
|
||||
Sêwirmend: 20,
|
||||
Hemwelatî: 10,
|
||||
Pêseng: 5, // Default for unlisted
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// ROLE CATEGORIZATION
|
||||
// ========================================
|
||||
|
||||
export const ROLE_CATEGORIES: Record<string, string[]> = {
|
||||
Government: ['Serok', 'SerokWeziran', 'Wezir', 'WezireDarayiye', 'WezireParez', 'WezireDad', 'WezireBelaw', 'WezireTend', 'WezireAva', 'WezireCand'],
|
||||
Legislature: ['Parlementer', 'SerokiMeclise'],
|
||||
Judiciary: ['EndameDiwane', 'Dadger', 'Dozger', 'Hiquqnas', 'Noter'],
|
||||
Administration: ['Xezinedar', 'Bacgir', 'Berdevk', 'Qeydkar', 'Balyoz', 'Mufetîs'],
|
||||
Technical: ['OperatorêTorê', 'PisporêEwlehiyaSîber', 'GerinendeyeDaneye', 'GerinendeyeCavkaniye'],
|
||||
Cultural: ['Mela', 'Feqî', 'ParêzvaneÇandî'],
|
||||
Education: ['Mamoste', 'Perwerdekar', 'Rewsenbîr'],
|
||||
Community: ['SerokêKomele', 'ModeratorêCivakê', 'Axa', 'Navbeynkar', 'Sêwirmend', 'Hekem'],
|
||||
Economic: ['Bazargan'],
|
||||
Leadership: ['RêveberêProjeyê', 'Pêseng'],
|
||||
Quality: ['KalîteKontrolker'],
|
||||
Citizen: ['Hemwelatî'],
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// TIKI QUERY FUNCTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Fetch user's tiki roles from blockchain
|
||||
* @param api - Polkadot API instance
|
||||
* @param address - User's substrate address
|
||||
* @returns Array of tiki role strings
|
||||
*/
|
||||
export const fetchUserTikis = async (
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<string[]> => {
|
||||
try {
|
||||
if (!api || !api.query.tiki) {
|
||||
console.warn('Tiki pallet not available on this chain');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Query UserTikis storage
|
||||
const tikis = await api.query.tiki.userTikis(address);
|
||||
const tikisArray = tikis.toJSON() as any[];
|
||||
|
||||
if (!tikisArray || tikisArray.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert from enum index to string names
|
||||
return tikisArray.map((tikiIndex: any) => {
|
||||
// Tikis are stored as enum variants
|
||||
if (typeof tikiIndex === 'string') {
|
||||
return tikiIndex;
|
||||
} else if (typeof tikiIndex === 'object' && tikiIndex !== null) {
|
||||
// Handle object variant format {variantName: null}
|
||||
return Object.keys(tikiIndex)[0];
|
||||
}
|
||||
return 'Unknown';
|
||||
}).filter((t: string) => t !== 'Unknown');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user tikis:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user is a citizen (has Hemwelatî tiki)
|
||||
* @param api - Polkadot API instance
|
||||
* @param address - User's substrate address
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isCitizen = async (
|
||||
api: ApiPromise,
|
||||
address: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
if (!api || !api.query.tiki) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const citizenNft = await api.query.tiki.citizenNft(address);
|
||||
return !citizenNft.isEmpty;
|
||||
} catch (error) {
|
||||
console.error('Error checking citizenship:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate total tiki score for a user
|
||||
* @param tikis - Array of tiki strings
|
||||
* @returns Total score
|
||||
*/
|
||||
export const calculateTikiScore = (tikis: string[]): number => {
|
||||
return tikis.reduce((total, tiki) => {
|
||||
return total + (TIKI_SCORES[tiki] || 5);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get primary role (highest scoring) from tikis
|
||||
* @param tikis - Array of tiki strings
|
||||
* @returns Primary role string
|
||||
*/
|
||||
export const getPrimaryRole = (tikis: string[]): string => {
|
||||
if (!tikis || tikis.length === 0) {
|
||||
return 'Member';
|
||||
}
|
||||
|
||||
// Find highest scoring role
|
||||
let primaryRole = tikis[0];
|
||||
let highestScore = TIKI_SCORES[tikis[0]] || 5;
|
||||
|
||||
for (const tiki of tikis) {
|
||||
const score = TIKI_SCORES[tiki] || 5;
|
||||
if (score > highestScore) {
|
||||
highestScore = score;
|
||||
primaryRole = tiki;
|
||||
}
|
||||
}
|
||||
|
||||
return primaryRole;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get display name for a tiki
|
||||
* @param tiki - Tiki string
|
||||
* @returns Display name
|
||||
*/
|
||||
export const getTikiDisplayName = (tiki: string): string => {
|
||||
return TIKI_DISPLAY_NAMES[tiki] || tiki;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all role categories for user's tikis
|
||||
* @param tikis - Array of tiki strings
|
||||
* @returns Array of category names
|
||||
*/
|
||||
export const getUserRoleCategories = (tikis: string[]): string[] => {
|
||||
const categories = new Set<string>();
|
||||
|
||||
for (const tiki of tikis) {
|
||||
for (const [category, roles] of Object.entries(ROLE_CATEGORIES)) {
|
||||
if (roles.includes(tiki)) {
|
||||
categories.add(category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(categories);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user has a specific tiki
|
||||
* @param tikis - Array of tiki strings
|
||||
* @param tiki - Tiki to check
|
||||
* @returns boolean
|
||||
*/
|
||||
export const hasTiki = (tikis: string[], tiki: string): boolean => {
|
||||
return tikis.includes(tiki);
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// DISPLAY HELPERS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get color for a tiki role
|
||||
* @param tiki - Tiki string
|
||||
* @returns Tailwind color class
|
||||
*/
|
||||
export const getTikiColor = (tiki: string): string => {
|
||||
const score = TIKI_SCORES[tiki] || 5;
|
||||
|
||||
if (score >= 200) return 'text-purple-500';
|
||||
if (score >= 150) return 'text-pink-500';
|
||||
if (score >= 100) return 'text-blue-500';
|
||||
if (score >= 70) return 'text-cyan-500';
|
||||
if (score >= 40) return 'text-teal-500';
|
||||
if (score >= 20) return 'text-green-500';
|
||||
return 'text-gray-500';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get emoji icon for a tiki category
|
||||
* @param tiki - Tiki string
|
||||
* @returns Emoji string
|
||||
*/
|
||||
export const getTikiEmoji = (tiki: string): string => {
|
||||
for (const [category, roles] of Object.entries(ROLE_CATEGORIES)) {
|
||||
if (roles.includes(tiki)) {
|
||||
switch (category) {
|
||||
case 'Government': return '👑';
|
||||
case 'Legislature': return '🏛️';
|
||||
case 'Judiciary': return '⚖️';
|
||||
case 'Administration': return '📋';
|
||||
case 'Technical': return '💻';
|
||||
case 'Cultural': return '📿';
|
||||
case 'Education': return '👨🏫';
|
||||
case 'Community': return '🤝';
|
||||
case 'Economic': return '💰';
|
||||
case 'Leadership': return '⭐';
|
||||
case 'Quality': return '✅';
|
||||
case 'Citizen': return '👤';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '👤';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get badge variant for a tiki
|
||||
* @param tiki - Tiki string
|
||||
* @returns Badge variant string
|
||||
*/
|
||||
export const getTikiBadgeVariant = (tiki: string): 'default' | 'secondary' | 'destructive' | 'outline' => {
|
||||
const score = TIKI_SCORES[tiki] || 5;
|
||||
|
||||
if (score >= 150) return 'default'; // Purple/blue for high ranks
|
||||
if (score >= 70) return 'secondary'; // Gray for mid ranks
|
||||
return 'outline'; // Outline for low ranks
|
||||
};
|
||||
@@ -1,314 +0,0 @@
|
||||
// ========================================
|
||||
// USDT Bridge Utilities
|
||||
// ========================================
|
||||
// Handles wUSDT minting, burning, and reserve management
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import { ASSET_IDS } from './wallet';
|
||||
import { getMultisigMembers, createMultisigTx } from './multisig';
|
||||
|
||||
// ========================================
|
||||
// CONSTANTS
|
||||
// ========================================
|
||||
|
||||
export const WUSDT_ASSET_ID = ASSET_IDS.WUSDT;
|
||||
export const WUSDT_DECIMALS = 6; // USDT has 6 decimals
|
||||
|
||||
// Withdrawal limits and timeouts
|
||||
export const WITHDRAWAL_LIMITS = {
|
||||
instant: {
|
||||
maxAmount: 1000, // $1,000
|
||||
delay: 0, // No delay
|
||||
},
|
||||
standard: {
|
||||
maxAmount: 10000, // $10,000
|
||||
delay: 3600, // 1 hour in seconds
|
||||
},
|
||||
large: {
|
||||
maxAmount: Infinity,
|
||||
delay: 86400, // 24 hours
|
||||
},
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// ASSET QUERIES
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get wUSDT balance for an account
|
||||
* @param api - Polkadot API instance
|
||||
* @param address - Account address
|
||||
* @returns Balance in human-readable format
|
||||
*/
|
||||
export async function getWUSDTBalance(api: ApiPromise, address: string): Promise<number> {
|
||||
try {
|
||||
const balance = await api.query.assets.account(WUSDT_ASSET_ID, address);
|
||||
|
||||
if (balance.isSome) {
|
||||
const balanceData = balance.unwrap().toJSON() as any;
|
||||
return Number(balanceData.balance) / Math.pow(10, WUSDT_DECIMALS);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (error) {
|
||||
console.error('Error fetching wUSDT balance:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total wUSDT supply
|
||||
* @param api - Polkadot API instance
|
||||
* @returns Total supply in human-readable format
|
||||
*/
|
||||
export async function getWUSDTTotalSupply(api: ApiPromise): Promise<number> {
|
||||
try {
|
||||
const assetDetails = await api.query.assets.asset(WUSDT_ASSET_ID);
|
||||
|
||||
if (assetDetails.isSome) {
|
||||
const details = assetDetails.unwrap().toJSON() as any;
|
||||
return Number(details.supply) / Math.pow(10, WUSDT_DECIMALS);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (error) {
|
||||
console.error('Error fetching wUSDT supply:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wUSDT asset metadata
|
||||
* @param api - Polkadot API instance
|
||||
* @returns Asset metadata
|
||||
*/
|
||||
export async function getWUSDTMetadata(api: ApiPromise) {
|
||||
try {
|
||||
const metadata = await api.query.assets.metadata(WUSDT_ASSET_ID);
|
||||
return metadata.toJSON();
|
||||
} catch (error) {
|
||||
console.error('Error fetching wUSDT metadata:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MULTISIG OPERATIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Create multisig transaction to mint wUSDT
|
||||
* @param api - Polkadot API instance
|
||||
* @param beneficiary - Who will receive the wUSDT
|
||||
* @param amount - Amount in human-readable format (e.g., 100.50 USDT)
|
||||
* @param signerAddress - Address of the signer creating this tx
|
||||
* @param specificAddresses - Addresses for non-unique multisig members
|
||||
* @returns Multisig transaction
|
||||
*/
|
||||
export async function createMintWUSDTTx(
|
||||
api: ApiPromise,
|
||||
beneficiary: string,
|
||||
amount: number,
|
||||
signerAddress: string,
|
||||
specificAddresses: Record<string, string> = {}
|
||||
) {
|
||||
// Convert to smallest unit
|
||||
const amountBN = BigInt(Math.floor(amount * Math.pow(10, WUSDT_DECIMALS)));
|
||||
|
||||
// Create the mint call
|
||||
const mintCall = api.tx.assets.mint(WUSDT_ASSET_ID, beneficiary, amountBN.toString());
|
||||
|
||||
// Get all multisig members
|
||||
const allMembers = await getMultisigMembers(api, specificAddresses);
|
||||
|
||||
// Other signatories (excluding current signer)
|
||||
const otherSignatories = allMembers.filter((addr) => addr !== signerAddress);
|
||||
|
||||
// Create multisig transaction
|
||||
return createMultisigTx(api, mintCall, otherSignatories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multisig transaction to burn wUSDT
|
||||
* @param api - Polkadot API instance
|
||||
* @param from - Who will have their wUSDT burned
|
||||
* @param amount - Amount in human-readable format
|
||||
* @param signerAddress - Address of the signer creating this tx
|
||||
* @param specificAddresses - Addresses for non-unique multisig members
|
||||
* @returns Multisig transaction
|
||||
*/
|
||||
export async function createBurnWUSDTTx(
|
||||
api: ApiPromise,
|
||||
from: string,
|
||||
amount: number,
|
||||
signerAddress: string,
|
||||
specificAddresses: Record<string, string> = {}
|
||||
) {
|
||||
const amountBN = BigInt(Math.floor(amount * Math.pow(10, WUSDT_DECIMALS)));
|
||||
|
||||
const burnCall = api.tx.assets.burn(WUSDT_ASSET_ID, from, amountBN.toString());
|
||||
|
||||
const allMembers = await getMultisigMembers(api, specificAddresses);
|
||||
const otherSignatories = allMembers.filter((addr) => addr !== signerAddress);
|
||||
|
||||
return createMultisigTx(api, burnCall, otherSignatories);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// WITHDRAWAL HELPERS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Calculate withdrawal delay based on amount
|
||||
* @param amount - Withdrawal amount in USDT
|
||||
* @returns Delay in seconds
|
||||
*/
|
||||
export function calculateWithdrawalDelay(amount: number): number {
|
||||
if (amount <= WITHDRAWAL_LIMITS.instant.maxAmount) {
|
||||
return WITHDRAWAL_LIMITS.instant.delay;
|
||||
} else if (amount <= WITHDRAWAL_LIMITS.standard.maxAmount) {
|
||||
return WITHDRAWAL_LIMITS.standard.delay;
|
||||
} else {
|
||||
return WITHDRAWAL_LIMITS.large.delay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get withdrawal tier name
|
||||
* @param amount - Withdrawal amount
|
||||
* @returns Tier name
|
||||
*/
|
||||
export function getWithdrawalTier(amount: number): string {
|
||||
if (amount <= WITHDRAWAL_LIMITS.instant.maxAmount) return 'Instant';
|
||||
if (amount <= WITHDRAWAL_LIMITS.standard.maxAmount) return 'Standard';
|
||||
return 'Large';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format delay time for display
|
||||
* @param seconds - Delay in seconds
|
||||
* @returns Human-readable format
|
||||
*/
|
||||
export function formatDelay(seconds: number): string {
|
||||
if (seconds === 0) return 'Instant';
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`;
|
||||
return `${Math.floor(seconds / 86400)} days`;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RESERVE CHECKING
|
||||
// ========================================
|
||||
|
||||
export interface ReserveStatus {
|
||||
wusdtSupply: number;
|
||||
offChainReserve: number; // This would come from off-chain oracle/API
|
||||
collateralRatio: number;
|
||||
isHealthy: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check reserve health
|
||||
* @param api - Polkadot API instance
|
||||
* @param offChainReserve - Off-chain USDT reserve amount (from treasury)
|
||||
* @returns Reserve status
|
||||
*/
|
||||
export async function checkReserveHealth(
|
||||
api: ApiPromise,
|
||||
offChainReserve: number
|
||||
): Promise<ReserveStatus> {
|
||||
const wusdtSupply = await getWUSDTTotalSupply(api);
|
||||
|
||||
const collateralRatio = wusdtSupply > 0 ? (offChainReserve / wusdtSupply) * 100 : 0;
|
||||
|
||||
return {
|
||||
wusdtSupply,
|
||||
offChainReserve,
|
||||
collateralRatio,
|
||||
isHealthy: collateralRatio >= 100, // At least 100% backed
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// EVENT MONITORING
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Subscribe to wUSDT mint events
|
||||
* @param api - Polkadot API instance
|
||||
* @param callback - Callback function for each mint event
|
||||
*/
|
||||
export function subscribeToMintEvents(
|
||||
api: ApiPromise,
|
||||
callback: (beneficiary: string, amount: number, txHash: string) => void
|
||||
) {
|
||||
return api.query.system.events((events) => {
|
||||
events.forEach((record) => {
|
||||
const { event } = record;
|
||||
|
||||
if (api.events.assets.Issued.is(event)) {
|
||||
const [assetId, beneficiary, amount] = event.data;
|
||||
|
||||
if (assetId.toNumber() === WUSDT_ASSET_ID) {
|
||||
const amountNum = Number(amount.toString()) / Math.pow(10, WUSDT_DECIMALS);
|
||||
callback(beneficiary.toString(), amountNum, record.hash.toHex());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to wUSDT burn events
|
||||
* @param api - Polkadot API instance
|
||||
* @param callback - Callback function for each burn event
|
||||
*/
|
||||
export function subscribeToBurnEvents(
|
||||
api: ApiPromise,
|
||||
callback: (account: string, amount: number, txHash: string) => void
|
||||
) {
|
||||
return api.query.system.events((events) => {
|
||||
events.forEach((record) => {
|
||||
const { event } = record;
|
||||
|
||||
if (api.events.assets.Burned.is(event)) {
|
||||
const [assetId, account, amount] = event.data;
|
||||
|
||||
if (assetId.toNumber() === WUSDT_ASSET_ID) {
|
||||
const amountNum = Number(amount.toString()) / Math.pow(10, WUSDT_DECIMALS);
|
||||
callback(account.toString(), amountNum, record.hash.toHex());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DISPLAY HELPERS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Format wUSDT amount for display
|
||||
* @param amount - Amount in smallest unit or human-readable
|
||||
* @param fromSmallestUnit - Whether input is in smallest unit
|
||||
* @returns Formatted string
|
||||
*/
|
||||
export function formatWUSDT(amount: number | string, fromSmallestUnit = false): string {
|
||||
const value = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
|
||||
if (fromSmallestUnit) {
|
||||
return (value / Math.pow(10, WUSDT_DECIMALS)).toFixed(2);
|
||||
}
|
||||
|
||||
return value.toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse human-readable USDT to smallest unit
|
||||
* @param amount - Human-readable amount
|
||||
* @returns Amount in smallest unit (BigInt)
|
||||
*/
|
||||
export function parseWUSDT(amount: number | string): bigint {
|
||||
const value = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
return BigInt(Math.floor(value * Math.pow(10, WUSDT_DECIMALS)));
|
||||
}
|
||||
+7
-17
@@ -1,24 +1,14 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
/**
|
||||
* Web-specific className utility (uses Tailwind merge)
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatNumber(value: number, decimals: number = 2): string {
|
||||
if (value === 0) return '0';
|
||||
if (value < 0.01) return '<0.01';
|
||||
|
||||
// For large numbers, use K, M, B suffixes
|
||||
if (value >= 1e9) {
|
||||
return (value / 1e9).toFixed(decimals) + 'B';
|
||||
}
|
||||
if (value >= 1e6) {
|
||||
return (value / 1e6).toFixed(decimals) + 'M';
|
||||
}
|
||||
if (value >= 1e3) {
|
||||
return (value / 1e3).toFixed(decimals) + 'K';
|
||||
}
|
||||
|
||||
return value.toFixed(decimals);
|
||||
}
|
||||
/**
|
||||
* Re-export formatNumber from shared utils
|
||||
*/
|
||||
export { formatNumber } from '@pezkuwi/utils/format';
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
// ========================================
|
||||
// PezkuwiChain - Substrate/Polkadot.js Configuration
|
||||
// ========================================
|
||||
// This file configures wallet connectivity for Substrate-based chains
|
||||
|
||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||
|
||||
// ========================================
|
||||
// NETWORK ENDPOINTS
|
||||
// ========================================
|
||||
export const NETWORK_ENDPOINTS = {
|
||||
local: import.meta.env.VITE_DEVELOPMENT_WS || 'ws://127.0.0.1:9944',
|
||||
testnet: import.meta.env.VITE_TESTNET_WS || 'wss://testnet.pezkuwichain.io',
|
||||
mainnet: import.meta.env.VITE_MAINNET_WS || 'wss://mainnet.pezkuwichain.io',
|
||||
staging: import.meta.env.VITE_STAGING_WS || 'wss://staging.pezkuwichain.io',
|
||||
beta: import.meta.env.VITE_BETA_WS || 'wss://beta.pezkuwichain.io',
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// CHAIN CONFIGURATION
|
||||
// ========================================
|
||||
export const CHAIN_CONFIG = {
|
||||
name: import.meta.env.VITE_CHAIN_NAME || 'PezkuwiChain',
|
||||
symbol: import.meta.env.VITE_CHAIN_TOKEN_SYMBOL || 'PEZ',
|
||||
decimals: parseInt(import.meta.env.VITE_CHAIN_TOKEN_DECIMALS || '12'),
|
||||
ss58Format: parseInt(import.meta.env.VITE_CHAIN_SS58_FORMAT || '42'),
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// SUBSTRATE ASSET IDs (Assets Pallet)
|
||||
// ========================================
|
||||
// ⚠️ IMPORTANT: HEZ is the native token and does NOT have an Asset ID
|
||||
// Only wrapped/asset tokens are listed here
|
||||
export const ASSET_IDS = {
|
||||
WHEZ: parseInt(import.meta.env.VITE_ASSET_WHEZ || '0'), // Wrapped HEZ
|
||||
PEZ: parseInt(import.meta.env.VITE_ASSET_PEZ || '1'), // PEZ utility token
|
||||
WUSDT: parseInt(import.meta.env.VITE_ASSET_WUSDT || '2'), // Wrapped USDT (multisig backed)
|
||||
USDT: parseInt(import.meta.env.VITE_ASSET_USDT || '3'),
|
||||
BTC: parseInt(import.meta.env.VITE_ASSET_BTC || '4'),
|
||||
ETH: parseInt(import.meta.env.VITE_ASSET_ETH || '5'),
|
||||
DOT: parseInt(import.meta.env.VITE_ASSET_DOT || '6'),
|
||||
} as const;
|
||||
|
||||
// ========================================
|
||||
// EXPLORER URLS
|
||||
// ========================================
|
||||
export const EXPLORER_URLS = {
|
||||
polkadotJs: import.meta.env.VITE_EXPLORER_URL || 'https://polkadot.js.org/apps/?rpc=',
|
||||
custom: import.meta.env.VITE_CUSTOM_EXPLORER_URL || 'https://explorer.pezkuwichain.io',
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// WALLET ERROR MESSAGES
|
||||
// ========================================
|
||||
export const WALLET_ERRORS = {
|
||||
NO_EXTENSION: 'No Polkadot.js extension detected. Please install Polkadot.js or compatible wallet.',
|
||||
NO_ACCOUNTS: 'No accounts found. Please create an account in your wallet extension.',
|
||||
CONNECTION_FAILED: 'Failed to connect wallet. Please try again.',
|
||||
TRANSACTION_FAILED: 'Transaction failed. Please check your balance and try again.',
|
||||
USER_REJECTED: 'User rejected the request.',
|
||||
INSUFFICIENT_BALANCE: 'Insufficient balance to complete transaction.',
|
||||
INVALID_ADDRESS: 'Invalid address format.',
|
||||
API_NOT_READY: 'Blockchain API not ready. Please wait...',
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Format Substrate address for display (SS58 format)
|
||||
* @param address - Full substrate address
|
||||
* @returns Shortened address string (e.g., "5GrwV...xQjz")
|
||||
*/
|
||||
export const formatAddress = (address: string): string => {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format balance from planck to human-readable format
|
||||
* @param balance - Balance in smallest unit (planck)
|
||||
* @param decimals - Token decimals (default 12 for PEZ)
|
||||
* @returns Formatted balance string
|
||||
*/
|
||||
export const formatBalance = (balance: string | number, decimals = 12): string => {
|
||||
if (!balance) return '0';
|
||||
const value = typeof balance === 'string' ? parseFloat(balance) : balance;
|
||||
return (value / Math.pow(10, decimals)).toFixed(4);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse human-readable amount to planck (smallest unit)
|
||||
* @param amount - Human-readable amount
|
||||
* @param decimals - Token decimals
|
||||
* @returns Amount in planck
|
||||
*/
|
||||
export const parseAmount = (amount: string | number, decimals = 12): bigint => {
|
||||
const value = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
return BigInt(Math.floor(value * Math.pow(10, decimals)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get asset symbol by ID
|
||||
* @param assetId - Asset ID from Assets pallet
|
||||
* @returns Asset symbol or 'UNKNOWN'
|
||||
*/
|
||||
export const getAssetSymbol = (assetId: number): string => {
|
||||
const entry = Object.entries(ASSET_IDS).find(([_, id]) => id === assetId);
|
||||
return entry ? entry[0] : 'UNKNOWN';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current network endpoint based on VITE_NETWORK env
|
||||
* @returns WebSocket endpoint URL
|
||||
*/
|
||||
export const getCurrentEndpoint = (): string => {
|
||||
const network = import.meta.env.VITE_NETWORK || 'local';
|
||||
return NETWORK_ENDPOINTS[network as keyof typeof NETWORK_ENDPOINTS] || NETWORK_ENDPOINTS.local;
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// TYPE DEFINITIONS
|
||||
// ========================================
|
||||
|
||||
export interface PolkadotWalletState {
|
||||
isConnected: boolean;
|
||||
accounts: InjectedAccountWithMeta[];
|
||||
selectedAccount: InjectedAccountWithMeta | null;
|
||||
balance: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const initialPolkadotWalletState: PolkadotWalletState = {
|
||||
isConnected: false,
|
||||
accounts: [],
|
||||
selectedAccount: null,
|
||||
balance: '0',
|
||||
error: null,
|
||||
};
|
||||
Reference in New Issue
Block a user