fix: tiki score, staking lookup, LP balance, teleport, DOT swap

- Fix tiki: use userTikis storage instead of userRoles
- Add tiki name to score mapping (welati=10, serok=50, etc)
- Improve staking ledger lookup with debug logging
- Fix LP balance fetching using poolId directly
- Change teleport placeholder from 0.5 to empty
- Add DOT token to swap list with 10 decimals
This commit is contained in:
2026-02-07 02:20:04 +03:00
parent c35c538678
commit 67b30daca8
7 changed files with 145 additions and 38 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pezkuwi-telegram-miniapp",
"version": "1.0.126",
"version": "1.0.127",
"type": "module",
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
"author": "Pezkuwichain Team",
+3 -3
View File
@@ -45,7 +45,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
const { hapticImpact, showAlert } = useTelegram();
const [targetChain, setTargetChain] = useState<TargetChain>('asset-hub');
const [amount, setAmount] = useState('0.5');
const [amount, setAmount] = useState('');
const [isTransferring, setIsTransferring] = useState(false);
const [txStatus, setTxStatus] = useState<'idle' | 'signing' | 'pending' | 'success' | 'error'>(
'idle'
@@ -244,7 +244,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
// Reset after success
setTimeout(() => {
setAmount('0.5');
setAmount('');
setTxStatus('idle');
onClose();
}, 2000);
@@ -405,7 +405,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
step="0.0001"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.5"
placeholder="Mîqdar"
className="w-full px-4 py-3 bg-muted rounded-xl text-lg font-mono"
disabled={isTransferring}
/>
+15 -7
View File
@@ -67,7 +67,8 @@ export function LPStakingModal({ isOpen, onClose }: LPStakingModalProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const poolData = value.toJSON() as any;
const lpTokenId = poolData.stakedAssetId?.interior?.x2?.[1]?.generalIndex ?? poolId;
// LP token ID in poolAssets pallet matches the pool ID (0, 1, 2)
const lpTokenId = poolId;
let userStaked = '0';
let pendingRewards = '0';
@@ -84,16 +85,23 @@ export function LPStakingModal({ isOpen, onClose }: LPStakingModalProps) {
const stakeData = stakeInfo.unwrap().toJSON();
userStaked = stakeData.amount || '0';
}
} catch (err) {
console.error('Error fetching stake info:', err);
}
// Fetch LP balance
// Fetch LP balance from poolAssets pallet
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const lpBal = await (assetHubApi.query.poolAssets as any).account(lpTokenId, address);
if (lpBal && lpBal.isSome) {
const lpData = lpBal.unwrap().toJSON();
lpBalance = lpData.balance || '0';
if (lpBal) {
// Handle both Option<AccountData> and direct AccountData
const lpData = lpBal.isSome ? lpBal.unwrap().toJSON() : lpBal.toJSON();
if (lpData && lpData.balance) {
lpBalance = lpData.balance.toString();
}
}
} catch {
// Ignore errors
} catch (err) {
console.error('Error fetching LP balance for pool', poolId, ':', err);
}
}
+18 -2
View File
@@ -19,6 +19,7 @@ const TOKENS = [
{ symbol: 'HEZ', name: 'Hezkurd', assetId: -1, decimals: 12, icon: '/tokens/HEZ.png' },
{ symbol: 'PEZ', name: 'Pezkuwi', assetId: 1, decimals: 12, icon: '/tokens/PEZ.png' },
{ symbol: 'USDT', name: 'Tether', assetId: 1000, decimals: 6, icon: '/tokens/USDT.png' },
{ symbol: 'DOT', name: 'Polkadot', assetId: 1001, decimals: 10, icon: '/tokens/DOT.png' },
];
// Native token ID for relay chain HEZ
@@ -51,6 +52,7 @@ export function SwapModal({ isOpen, onClose }: SwapModalProps) {
HEZ: '0',
PEZ: '0',
USDT: '0',
DOT: '0',
});
// Fetch balances from Asset Hub (where swaps happen)
@@ -84,11 +86,19 @@ export function SwapModal({ isOpen, onClose }: SwapModalProps) {
? (parseInt(usdtResult.unwrap().balance.toString()) / 1e6).toFixed(2)
: '0.00';
// DOT balance (Asset 1001, 10 decimals)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dotResult = await (assetHubApi.query.assets as any).account(1001, keypair.address);
const dotBalance = dotResult.isSome
? (parseInt(dotResult.unwrap().balance.toString()) / 1e10).toFixed(4)
: '0.0000';
// Update all balances at once
setBalances({
HEZ: hezBalance,
PEZ: pezBalance,
USDT: usdtBalance,
DOT: dotBalance,
});
} catch (err) {
console.error('Failed to fetch balances:', err);
@@ -127,8 +137,14 @@ export function SwapModal({ isOpen, onClose }: SwapModalProps) {
if (poolInfo && !poolInfo.isEmpty) {
// Get quote from runtime API
const decimals1 = asset1 === 1000 ? 6 : 12;
const decimals2 = asset2 === 1000 ? 6 : 12;
// USDT has 6 decimals, DOT has 10 decimals, others have 12
const getDecimals = (id: number) => {
if (id === 1000) return 6; // USDT
if (id === 1001) return 10; // DOT
return 12; // HEZ, PEZ
};
const decimals1 = getDecimals(asset1);
const decimals2 = getDecimals(asset2);
const oneUnit = BigInt(Math.pow(10, decimals1));
const quote = await (
+101 -22
View File
@@ -64,6 +64,7 @@ function saveStakingTrackingData(data: StakingTrackingData): void {
/**
* Fetch staking details directly from Relay Chain
* In newer Substrate versions, ledger is keyed by stash address
*/
export async function fetchRelayStakingDetails(
relayApi: ApiPromise,
@@ -71,36 +72,64 @@ export async function fetchRelayStakingDetails(
): Promise<{ stakedAmount: bigint; nominationsCount: number } | null> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(relayApi?.query as any)?.staking) return null;
if (!(relayApi?.query as any)?.staking) {
console.log('[Staking] staking pallet not found');
return null;
}
let stashAddress = address;
let active = 0n;
// In newer Substrate, ledger is keyed by stash address directly
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let ledger = await (relayApi.query.staking as any).ledger?.(address);
let stashAddress = address;
// If no ledger, check if this is a stash account
if (!ledger || ledger.isEmpty || ledger.isNone) {
// Check if ledger exists and has data
if (ledger && !ledger.isEmpty && !ledger.isNone) {
// Ledger might be wrapped in Option
const unwrapped = ledger.isSome ? ledger.unwrap() : ledger;
const ledgerJson = unwrapped.toJSON() as { active?: string | number; stash?: string };
console.log('[Staking] Ledger found for', address, ':', ledgerJson);
active = BigInt(ledgerJson?.active || 0);
if (ledgerJson?.stash) {
stashAddress = ledgerJson.stash;
}
} else {
// Fallback: check if this is a stash account with a controller
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bonded = await (relayApi.query.staking as any).bonded?.(address);
if (bonded && !bonded.isEmpty && !bonded.isNone) {
const controller = bonded.toString();
console.log('[Staking] Address', address, 'is stash, controller:', controller);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ledger = await (relayApi.query.staking as any).ledger?.(controller);
stashAddress = address;
if (ledger && !ledger.isEmpty && !ledger.isNone) {
const unwrapped = ledger.isSome ? ledger.unwrap() : ledger;
const ledgerJson = unwrapped.toJSON() as { active?: string | number };
console.log('[Staking] Ledger from controller:', ledgerJson);
active = BigInt(ledgerJson?.active || 0);
}
} else {
console.log('[Staking] No ledger or bonded found for', address);
}
}
if (!ledger || ledger.isEmpty || ledger.isNone) {
if (active === 0n) {
return null;
}
const ledgerJson = ledger.toJSON() as { active?: string | number };
const active = BigInt(ledgerJson?.active || 0);
// Get nominations
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nominations = await (relayApi.query.staking as any).nominators?.(stashAddress);
const nominationsJson = nominations?.toJSON() as { targets?: unknown[] } | null;
const nominationsCount = nominationsJson?.targets?.length || 0;
console.log(
'[Staking] Final result - active:',
active.toString(),
'nominations:',
nominationsCount
);
return {
stakedAmount: active,
nominationsCount,
@@ -224,25 +253,68 @@ const TIKI_ROLE_SCORES: Record<number, number> = {
};
/**
* Fetch user's tiki roles from People Chain
* Tiki role name to score mapping
* Welati (citizen) is the basic role with score 10
*/
const TIKI_NAME_SCORES: Record<string, number> = {
welati: 10,
parlementer: 30,
serokimeclise: 40,
serok: 50,
wezir: 40,
endamediwane: 30,
dadger: 35,
dozger: 35,
mamoste: 25,
perwerdekar: 25,
bazargan: 20,
};
/**
* Fetch user's tikis from People Chain
* Storage: tiki.userTikis(address) -> Vec<TikiRole>
*/
export async function fetchUserTikis(peopleApi: ApiPromise, address: string): Promise<TikiInfo[]> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(peopleApi?.query as any)?.tiki) return [];
if (!(peopleApi?.query as any)?.tiki) {
console.log('[Tiki] tiki pallet not found');
return [];
}
// Try userTikis first (actual storage name)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await (peopleApi.query.tiki as any).userRoles?.(address);
let result = await (peopleApi.query.tiki as any).userTikis?.(address);
if (!result || result.isEmpty) return [];
// Fallback to userRoles if userTikis doesn't exist
if (!result && (peopleApi.query.tiki as any).userRoles) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result = await (peopleApi.query.tiki as any).userRoles?.(address);
}
if (!result || result.isEmpty) {
console.log('[Tiki] No tikis found for', address);
return [];
}
// Result is Vec<TikiRole> which are enum variants as strings
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const roles = result.toJSON() as any[];
return roles.map((role) => ({
roleId: role.roleId || role.role_id || 0,
level: role.level || 0,
name: role.name || 'Unknown',
}));
const tikis = result.toJSON() as any[];
console.log('[Tiki] Raw tikis for', address, ':', tikis);
return tikis.map((tiki, index) => {
// Tiki can be a string (enum variant name) or object
const name = typeof tiki === 'string' ? tiki : tiki.name || tiki.role || 'Unknown';
const nameLower = name.toLowerCase();
const score = TIKI_NAME_SCORES[nameLower] || 10; // Default to 10 if unknown
return {
roleId: index + 1,
level: 1,
name: name,
score: score,
};
});
} catch (err) {
console.error('Failed to fetch tiki roles:', err);
return [];
@@ -250,7 +322,8 @@ export async function fetchUserTikis(peopleApi: ApiPromise, address: string): Pr
}
/**
* Calculate tiki score from user's roles
* Calculate tiki score from user's tikis
* Uses the score property set during fetch, or looks up by name
*/
export function calculateTikiScore(tikis: TikiInfo[]): number {
if (!tikis.length) return 0;
@@ -258,10 +331,16 @@ export function calculateTikiScore(tikis: TikiInfo[]): number {
// Get highest role score
let maxScore = 0;
for (const tiki of tikis) {
const roleScore = TIKI_ROLE_SCORES[tiki.roleId] || 0;
maxScore = Math.max(maxScore, roleScore);
// Use score from tiki if available, otherwise lookup by roleId or name
const tikiScore =
(tiki as TikiInfo & { score?: number }).score ||
TIKI_ROLE_SCORES[tiki.roleId] ||
TIKI_NAME_SCORES[tiki.name.toLowerCase()] ||
10; // Default welati score
maxScore = Math.max(maxScore, tikiScore);
}
console.log('[Tiki] Calculated score:', maxScore, 'from tikis:', tikis);
return Math.min(maxScore, 50); // Capped at 50
}
+4
View File
@@ -101,12 +101,16 @@ export function RewardsSection() {
return;
}
console.log('[Scores] Fetching scores for', address);
console.log('[Scores] API connected:', !!api, 'People API:', !!peopleApi);
setScoresLoading(true);
try {
const [scores, staking] = await Promise.all([
getAllScoresWithFallback(peopleApi, api, address),
api ? getFrontendStakingScore(api, address) : Promise.resolve(null),
]);
console.log('[Scores] Results:', { scores, staking });
setUserScores(scores);
setStakingDetails(staking);
} catch (err) {
+3 -3
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.126",
"buildTime": "2026-02-06T23:07:37.278Z",
"buildNumber": 1770419257279
"version": "1.0.127",
"buildTime": "2026-02-06T23:20:04.537Z",
"buildNumber": 1770420004537
}