mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-07-24 03:35:42 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fda28c2d5 | |||
| c4384c8eb9 | |||
| 0dd4d3d62b | |||
| fd6b2cc28e | |||
| 75114e7cb1 | |||
| 2017ae77da | |||
| 97e5723aa5 | |||
| 039ce697c8 | |||
| 39ff9e959f | |||
| 704a46f459 | |||
| d14c8f1a3c | |||
| 94c6347521 | |||
| 18cb20a810 | |||
| a5bdebe755 | |||
| c9211a9e34 | |||
| abd4dc7189 |
@@ -0,0 +1,47 @@
|
||||
name: Auto Merge
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Find and merge master → main PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}"
|
||||
echo "Workflow ran on branch: $HEAD_BRANCH"
|
||||
|
||||
if [ "$HEAD_BRANCH" != "master" ]; then
|
||||
echo "Not a master branch PR, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_NUMBER=$(gh pr list \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--base main \
|
||||
--head master \
|
||||
--state open \
|
||||
--json number \
|
||||
--jq '.[0].number')
|
||||
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "No open PR from master to main found, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Merging PR #$PR_NUMBER"
|
||||
gh pr merge "$PR_NUMBER" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--merge \
|
||||
--delete-branch=false
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Auto PR (master → main)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
create-pr:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create or update PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Check if there's already an open PR from master to main
|
||||
EXISTING_PR=$(gh pr list --base main --head master --state open --json number --jq '.[0].number')
|
||||
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR #$EXISTING_PR already exists — new commits will appear automatically"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Creating new PR: master → main"
|
||||
if gh pr create \
|
||||
--base main \
|
||||
--head master \
|
||||
--title "Sync: master → main" \
|
||||
--body "Automated PR to sync master branch changes to main.
|
||||
|
||||
This PR was created automatically and will be merged once CI checks pass."; then
|
||||
echo "PR created successfully"
|
||||
else
|
||||
echo "PR creation skipped (branches may already be in sync)"
|
||||
fi
|
||||
@@ -55,3 +55,11 @@ jobs:
|
||||
source: 'dist/*'
|
||||
target: '/var/www/telegram.pezkiwi.app'
|
||||
strip_components: 1
|
||||
|
||||
- name: Cleanup old assets on VPS
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.VPS2_HOST }}
|
||||
username: ${{ secrets.VPS2_USER }}
|
||||
key: ${{ secrets.VPS2_SSH_KEY }}
|
||||
script: bash /opt/cleanup-miniapp.sh
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Sync main and master branches
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Sync branches
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
echo "main was updated, syncing master..."
|
||||
git checkout master
|
||||
git reset --hard origin/main
|
||||
git push origin master --force
|
||||
elif [ "${{ github.ref }}" = "refs/heads/master" ]; then
|
||||
echo "master was updated, syncing main..."
|
||||
git checkout main
|
||||
git reset --hard origin/master
|
||||
git push origin main --force
|
||||
fi
|
||||
Generated
+1125
-452
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pezkuwi-telegram-miniapp",
|
||||
"version": "1.0.229",
|
||||
"version": "1.0.239",
|
||||
"type": "module",
|
||||
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
|
||||
"author": "Pezkuwichain Team",
|
||||
@@ -13,7 +13,7 @@
|
||||
"build:patch": "node scripts/bump-version.mjs patch && tsc && vite build",
|
||||
"build:no-bump": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 30",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext ts,tsx --fix",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
|
||||
# PezkuwiChain Telegram Bot Webhook Setup
|
||||
# Run this script from a machine that can access Telegram API
|
||||
# Usage: BOT_TOKEN="..." ./scripts/setup-telegram-webhook.sh
|
||||
|
||||
if [ -z "$BOT_TOKEN" ]; then
|
||||
echo "Error: BOT_TOKEN environment variable is not set."
|
||||
echo "Usage: BOT_TOKEN=\"<token>\" ./scripts/setup-telegram-webhook.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BOT_TOKEN="8548408481:AAEsoyiVBllk_x0T3Jelj8N8VrUiuc9jXQw"
|
||||
WEBHOOK_URL="https://vbhftvdayqfmcgmzdxfv.supabase.co/functions/v1/telegram-bot"
|
||||
|
||||
echo "Setting up Telegram webhook..."
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
/**
|
||||
* In-App Citizenship Application Modal
|
||||
* Self-contained modal with 3 steps: form → processing → success
|
||||
* Uses the already-connected wallet keypair (no seed phrase needed)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { X, Plus, Trash2, Shield, CheckCircle, Clock, ArrowRight, AlertCircle } from 'lucide-react';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import { useTranslation } from '@/i18n';
|
||||
import { KurdistanSun } from '@/components/KurdistanSun';
|
||||
import { formatAddress } from '@/lib/utils';
|
||||
import type { Region, MaritalStatus, ChildInfo } from '@/lib/citizenship';
|
||||
import {
|
||||
calculateIdentityHash,
|
||||
saveCitizenshipLocally,
|
||||
uploadToIPFS,
|
||||
applyCitizenship,
|
||||
} from '@/lib/citizenship';
|
||||
|
||||
interface CitizenshipModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type Step = 'form' | 'processing' | 'success';
|
||||
|
||||
const REGIONS: { value: Region; labelKey: string }[] = [
|
||||
{ value: 'bakur', labelKey: 'citizen.regionBakur' },
|
||||
{ value: 'basur', labelKey: 'citizen.regionBasur' },
|
||||
{ value: 'rojava', labelKey: 'citizen.regionRojava' },
|
||||
{ value: 'rojhelat', labelKey: 'citizen.regionRojhelat' },
|
||||
{ value: 'kurdistan_a_sor', labelKey: 'citizen.regionKurdistanASor' },
|
||||
{ value: 'diaspora', labelKey: 'citizen.regionDiaspora' },
|
||||
];
|
||||
|
||||
export function CitizenshipModal({ isOpen, onClose }: CitizenshipModalProps) {
|
||||
const { peopleApi, keypair, address } = useWallet();
|
||||
const { hapticImpact, hapticNotification } = useTelegram();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [step, setStep] = useState<Step>('form');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Form fields
|
||||
const [fullName, setFullName] = useState('');
|
||||
const [fatherName, setFatherName] = useState('');
|
||||
const [grandfatherName, setGrandfatherName] = useState('');
|
||||
const [motherName, setMotherName] = useState('');
|
||||
const [tribe, setTribe] = useState('');
|
||||
const [maritalStatus, setMaritalStatus] = useState<MaritalStatus>('nezewici');
|
||||
const [childrenCount, setChildrenCount] = useState(0);
|
||||
const [children, setChildren] = useState<ChildInfo[]>([]);
|
||||
const [region, setRegion] = useState<Region | ''>('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [profession, setProfession] = useState('');
|
||||
const [referrerAddress, setReferrerAddress] = useState('');
|
||||
const [consent, setConsent] = useState(false);
|
||||
|
||||
// Success data
|
||||
const [identityHash, setIdentityHash] = useState('');
|
||||
|
||||
// Processing cancel ref
|
||||
const cancelledRef = useRef(false);
|
||||
|
||||
// Reset state when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setStep('form');
|
||||
setError('');
|
||||
setFullName('');
|
||||
setFatherName('');
|
||||
setGrandfatherName('');
|
||||
setMotherName('');
|
||||
setTribe('');
|
||||
setMaritalStatus('nezewici');
|
||||
setChildrenCount(0);
|
||||
setChildren([]);
|
||||
setRegion('');
|
||||
setEmail('');
|
||||
setProfession('');
|
||||
setReferrerAddress('');
|
||||
setConsent(false);
|
||||
setIdentityHash('');
|
||||
cancelledRef.current = false;
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// --- Form handlers ---
|
||||
|
||||
const handleMaritalChange = (status: MaritalStatus) => {
|
||||
hapticImpact('light');
|
||||
setMaritalStatus(status);
|
||||
if (status === 'nezewici') {
|
||||
setChildrenCount(0);
|
||||
setChildren([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChildrenCountChange = (count: number) => {
|
||||
const c = Math.max(0, Math.min(20, count));
|
||||
setChildrenCount(c);
|
||||
setChildren((prev) => {
|
||||
if (c > prev.length) {
|
||||
return [
|
||||
...prev,
|
||||
...Array.from({ length: c - prev.length }, () => ({ name: '', birthYear: 2000 })),
|
||||
];
|
||||
}
|
||||
return prev.slice(0, c);
|
||||
});
|
||||
};
|
||||
|
||||
const updateChild = (index: number, field: keyof ChildInfo, value: string | number) => {
|
||||
setChildren((prev) =>
|
||||
prev.map((child, i) => (i === index ? { ...child, [field]: value } : child))
|
||||
);
|
||||
};
|
||||
|
||||
const addChild = () => {
|
||||
hapticImpact('light');
|
||||
handleChildrenCountChange(childrenCount + 1);
|
||||
};
|
||||
|
||||
const removeChild = (index: number) => {
|
||||
hapticImpact('light');
|
||||
setChildren((prev) => prev.filter((_, i) => i !== index));
|
||||
setChildrenCount((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
setError('');
|
||||
|
||||
if (
|
||||
!fullName ||
|
||||
!fatherName ||
|
||||
!grandfatherName ||
|
||||
!motherName ||
|
||||
!tribe ||
|
||||
!region ||
|
||||
!email ||
|
||||
!profession
|
||||
) {
|
||||
setError(t('citizen.fillAllFields'));
|
||||
hapticNotification('error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consent) {
|
||||
setError(t('citizen.acceptConsent'));
|
||||
hapticNotification('error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!peopleApi) {
|
||||
setError(t('citizen.peopleChainNotConnected'));
|
||||
hapticNotification('error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keypair || !address) {
|
||||
setError(t('citizen.walletNotConnected'));
|
||||
hapticNotification('error');
|
||||
return;
|
||||
}
|
||||
|
||||
hapticImpact('medium');
|
||||
setStep('processing');
|
||||
};
|
||||
|
||||
// --- Processing logic ---
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 'processing') return;
|
||||
|
||||
cancelledRef.current = false;
|
||||
|
||||
const process = async () => {
|
||||
try {
|
||||
if (!peopleApi || !keypair || !address) {
|
||||
setError(t('citizen.walletNotConnected'));
|
||||
setStep('form');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build citizenship data for local save
|
||||
const citizenshipData = {
|
||||
fullName,
|
||||
fatherName,
|
||||
grandfatherName,
|
||||
motherName,
|
||||
tribe,
|
||||
maritalStatus,
|
||||
childrenCount: maritalStatus === 'zewici' ? childrenCount : undefined,
|
||||
children: maritalStatus === 'zewici' && children.length > 0 ? children : undefined,
|
||||
region: region as Region,
|
||||
email,
|
||||
profession,
|
||||
referrerAddress: referrerAddress || undefined,
|
||||
walletAddress: address,
|
||||
seedPhrase: '',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Mock IPFS upload
|
||||
const ipfsCid = await uploadToIPFS(citizenshipData);
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
// Calculate identity hash
|
||||
const hash = calculateIdentityHash(fullName, email, [ipfsCid]);
|
||||
if (cancelledRef.current) return;
|
||||
setIdentityHash(hash);
|
||||
|
||||
// Save encrypted data locally
|
||||
saveCitizenshipLocally(citizenshipData);
|
||||
|
||||
// Sign and submit extrinsic
|
||||
const result = await applyCitizenship(peopleApi, keypair, hash, referrerAddress || null);
|
||||
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
if (result.success) {
|
||||
hapticNotification('success');
|
||||
setStep('success');
|
||||
} else {
|
||||
hapticNotification('error');
|
||||
setError(result.error || t('citizen.submissionFailed'));
|
||||
setStep('form');
|
||||
}
|
||||
} catch (err) {
|
||||
if (cancelledRef.current) return;
|
||||
hapticNotification('error');
|
||||
setError(err instanceof Error ? err.message : t('citizen.submissionFailed'));
|
||||
setStep('form');
|
||||
}
|
||||
};
|
||||
|
||||
process();
|
||||
|
||||
return () => {
|
||||
cancelledRef.current = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [step]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const inputClass = 'w-full px-4 py-3 bg-muted rounded-xl text-sm';
|
||||
const labelClass = 'text-sm text-muted-foreground mb-1 block';
|
||||
|
||||
// --- Processing Step ---
|
||||
if (step === 'processing') {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md bg-card rounded-2xl shadow-xl border border-border p-6">
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-6">
|
||||
<KurdistanSun size={100} />
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-lg font-medium">{t('citizen.applyingCitizenship')}</p>
|
||||
<p className="text-sm text-muted-foreground">{fullName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Success Step ---
|
||||
if (step === 'success') {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md bg-card rounded-2xl shadow-xl border border-border overflow-hidden max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex flex-col items-center p-6 space-y-6">
|
||||
{/* Success Icon */}
|
||||
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-500" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-xl font-bold">{t('citizen.applicationSuccess')}</h2>
|
||||
|
||||
{/* 3-Step Process */}
|
||||
<div className="w-full space-y-3">
|
||||
<div className="flex items-start gap-3 p-3 bg-green-500/10 border border-green-500/30 rounded-xl">
|
||||
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-green-400">{t('citizen.stepApplicationSent')}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-xl">
|
||||
<Clock className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-yellow-400">{t('citizen.stepReferrerApproval')}</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-3 bg-muted/50 border border-border rounded-xl">
|
||||
<ArrowRight className="w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-muted-foreground">{t('citizen.stepConfirm')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Application Info */}
|
||||
<div className="w-full bg-muted/50 rounded-2xl p-5 space-y-4 border border-border">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t('citizen.identityHash')}</p>
|
||||
<p className="text-sm font-mono break-all">{formatAddress(identityHash)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t('citizen.walletAddress')}</p>
|
||||
<p className="text-sm font-mono">{formatAddress(address || '')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Next Steps Info */}
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{t('citizen.nextStepsInfo')}
|
||||
</p>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-3 bg-primary text-primary-foreground rounded-xl font-semibold"
|
||||
>
|
||||
{t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Form Step ---
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
|
||||
<div className="w-full max-w-md bg-card rounded-2xl shadow-xl border border-border overflow-hidden max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-border sticky top-0 bg-card z-10">
|
||||
<h2 className="text-lg font-semibold">{t('citizen.pageTitle')}</h2>
|
||||
<button onClick={onClose} className="p-2 rounded-full hover:bg-muted">
|
||||
<X className="w-5 h-5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form Content */}
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Privacy Notice */}
|
||||
<div className="flex gap-3 p-3 bg-blue-500/10 border border-blue-500/30 rounded-xl">
|
||||
<Shield className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-blue-300">{t('citizen.privacyNotice')}</p>
|
||||
</div>
|
||||
|
||||
{/* Full Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.fullName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.fullNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Father's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.fatherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fatherName}
|
||||
onChange={(e) => setFatherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.fatherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Grandfather's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.grandfatherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={grandfatherName}
|
||||
onChange={(e) => setGrandfatherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.grandfatherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mother's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.motherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={motherName}
|
||||
onChange={(e) => setMotherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.motherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tribe */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.tribe')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tribe}
|
||||
onChange={(e) => setTribe(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.tribePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Marital Status */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.maritalStatus')}</label>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMaritalChange('nezewici')}
|
||||
className={`flex-1 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
maritalStatus === 'nezewici'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('citizen.single')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMaritalChange('zewici')}
|
||||
className={`flex-1 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
maritalStatus === 'zewici'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('citizen.married')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Children (if married) */}
|
||||
{maritalStatus === 'zewici' && (
|
||||
<div className="space-y-3">
|
||||
<label className={labelClass}>{t('citizen.childrenCount')}</label>
|
||||
{children.map((child, index) => (
|
||||
<div key={index} className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t('citizen.childName', { index: String(index + 1) })}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={child.name}
|
||||
onChange={(e) => updateChild(index, 'name', e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.childNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t('citizen.childBirthYear')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={child.birthYear}
|
||||
onChange={(e) =>
|
||||
updateChild(index, 'birthYear', parseInt(e.target.value) || 2000)
|
||||
}
|
||||
className={inputClass}
|
||||
min={1950}
|
||||
max={2026}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeChild(index)}
|
||||
className="p-3 text-red-400 hover:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addChild}
|
||||
className="flex items-center gap-2 text-sm text-primary"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('citizen.addChild')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Region */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.region')}</label>
|
||||
<select
|
||||
value={region}
|
||||
onChange={(e) => {
|
||||
setRegion(e.target.value as Region);
|
||||
hapticImpact('light');
|
||||
}}
|
||||
className={`${inputClass} appearance-none`}
|
||||
>
|
||||
<option value="">{t('citizen.regionPlaceholder')}</option>
|
||||
{REGIONS.map((r) => (
|
||||
<option key={r.value} value={r.value}>
|
||||
{t(r.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.email')}</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.emailPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profession */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.profession')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={profession}
|
||||
onChange={(e) => setProfession(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.professionPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Referrer Address */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.referrerAddress')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={referrerAddress}
|
||||
onChange={(e) => setReferrerAddress(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.referrerPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Consent */}
|
||||
<label className="flex items-start gap-3 p-3 bg-muted/50 rounded-xl cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={consent}
|
||||
onChange={(e) => setConsent(e.target.checked)}
|
||||
className="mt-1 w-5 h-5 rounded accent-primary flex-shrink-0"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">{t('citizen.consentCheckbox')}</span>
|
||||
</label>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
onClick={handleFormSubmit}
|
||||
disabled={!consent || !fullName || !region || !email}
|
||||
className="w-full py-3 bg-primary text-primary-foreground rounded-xl font-semibold disabled:opacity-50"
|
||||
>
|
||||
{t('citizen.submit')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,14 +13,14 @@ interface SocialLink {
|
||||
const SOCIAL_LINKS: SocialLink[] = [
|
||||
{
|
||||
name: 'Instagram',
|
||||
url: 'https://www.instagram.com/pezkuwichain',
|
||||
url: 'https://www.instagram.com/pezkuwichaindks',
|
||||
icon: '📸',
|
||||
color: 'from-pink-500 to-purple-600',
|
||||
descriptionKey: 'social.instagram',
|
||||
},
|
||||
{
|
||||
name: 'TikTok',
|
||||
url: 'https://www.tiktok.com/@pezkuwi.chain',
|
||||
url: 'https://www.tiktok.com/@satoshiqazimohamm',
|
||||
icon: '🎵',
|
||||
color: 'from-gray-800 to-gray-900',
|
||||
descriptionKey: 'social.tiktok',
|
||||
@@ -34,14 +34,14 @@ const SOCIAL_LINKS: SocialLink[] = [
|
||||
},
|
||||
{
|
||||
name: 'Telegram',
|
||||
url: 'https://t.me/dijitalkurdistan',
|
||||
url: 'https://t.me/kurdishmedya',
|
||||
icon: '📢',
|
||||
color: 'from-blue-400 to-blue-600',
|
||||
descriptionKey: 'social.telegram',
|
||||
},
|
||||
{
|
||||
name: 'X (Twitter)',
|
||||
url: 'https://x.com/pezkuwichain',
|
||||
url: 'https://x.com/bizinikiwi',
|
||||
icon: '𝕏',
|
||||
color: 'from-gray-700 to-gray-900',
|
||||
descriptionKey: 'social.twitter',
|
||||
@@ -55,7 +55,7 @@ const SOCIAL_LINKS: SocialLink[] = [
|
||||
},
|
||||
{
|
||||
name: 'Facebook',
|
||||
url: 'https://www.facebook.com/people/Pezkuwi-Chain/61587122224932/',
|
||||
url: 'https://www.facebook.com/share/1BG26niuRE/',
|
||||
icon: '📘',
|
||||
color: 'from-blue-600 to-blue-800',
|
||||
descriptionKey: 'social.facebook',
|
||||
|
||||
@@ -79,6 +79,9 @@ export function DepositWithdrawModal({
|
||||
|
||||
const handleDeposit = async () => {
|
||||
if (!sessionToken || !depositAmount || !assetHubApi || !keypair) return;
|
||||
// Capture the guaranteed-non-null API so the narrowing survives inside the
|
||||
// async signAndSend callback below (TS loses it across the closure boundary).
|
||||
const api = assetHubApi;
|
||||
|
||||
const amount = parseFloat(depositAmount);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
@@ -120,7 +123,7 @@ export function DepositWithdrawModal({
|
||||
async (result: any) => {
|
||||
if (result.dispatchError) {
|
||||
if (result.dispatchError.isModule) {
|
||||
const decoded = assetHubApi!.registry.findMetaError(result.dispatchError.asModule);
|
||||
const decoded = api.registry.findMetaError(result.dispatchError.asModule);
|
||||
reject(new Error(`${decoded.section}.${decoded.name}`));
|
||||
} else {
|
||||
reject(new Error(result.dispatchError.toString()));
|
||||
@@ -130,7 +133,7 @@ export function DepositWithdrawModal({
|
||||
if (result.status.isFinalized) {
|
||||
try {
|
||||
// Get block number from finalized block hash for fast verification
|
||||
const header = await assetHubApi!.rpc.chain.getHeader(result.status.asFinalized);
|
||||
const header = await api.rpc.chain.getHeader(result.status.asFinalized);
|
||||
resolve({
|
||||
txHash: result.txHash.toHex(),
|
||||
blockNumber: header.number.toNumber(),
|
||||
|
||||
@@ -424,9 +424,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
|
||||
}`}
|
||||
/>
|
||||
<div className="text-sm font-medium">{chain.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t(chain.description as any)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{t(chain.description)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -505,7 +503,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
|
||||
targetChain === 'asset-hub' ? 'text-blue-400' : 'text-purple-400'
|
||||
}`}
|
||||
>
|
||||
{t('fees.minRecommended', { description: t(selectedChain.description as any) })}
|
||||
{t('fees.minRecommended', { description: t(selectedChain.description) })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -133,6 +133,10 @@ export function HEZStakingModal({ isOpen, onClose }: HEZStakingModalProps) {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// `t` is intentionally omitted: it is only used for error messages and its
|
||||
// identity changes only on language switch — including it would re-run the
|
||||
// blockchain fetch (extra RPC calls) on every language change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assetHubApi, address]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -179,6 +179,10 @@ export function LPStakingModal({ isOpen, onClose }: LPStakingModalProps) {
|
||||
};
|
||||
|
||||
fetchPools();
|
||||
// `t` is intentionally omitted: it is only used for error messages and its
|
||||
// identity changes only on language switch — including it would re-run the
|
||||
// blockchain fetch (extra RPC calls) on every language change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [assetHubApi, isOpen, address, selectedPool]);
|
||||
|
||||
const formatAmount = (amount: string, decimals: number = 12): string => {
|
||||
|
||||
@@ -50,6 +50,26 @@ const formatAssetLocation = (id: number) => {
|
||||
return { parents: 0, interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: id }] } };
|
||||
};
|
||||
|
||||
// Minimal shapes for the dynamic @pezkuwi/api results we read here. These
|
||||
// pallets/runtime-calls are not in the base typings, so we describe just the
|
||||
// fields actually used and cast via `as unknown as`.
|
||||
type AccountInfoResult = { data: { free: { toString(): string } } };
|
||||
type AssetAccountResult = {
|
||||
isSome: boolean;
|
||||
unwrap(): { balance: { toString(): string } };
|
||||
};
|
||||
type QuoteResult = { isNone: boolean; unwrap(): { toString(): string } };
|
||||
type AssetConversionCall = {
|
||||
assetConversionApi: {
|
||||
quotePriceExactTokensForTokens: (
|
||||
asset0: ReturnType<typeof formatAssetLocation>,
|
||||
asset1: ReturnType<typeof formatAssetLocation>,
|
||||
amount: string,
|
||||
includeFee: boolean
|
||||
) => Promise<QuoteResult>;
|
||||
};
|
||||
};
|
||||
|
||||
export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
const { assetHubApi, keypair } = useWallet();
|
||||
const { hapticImpact, hapticNotification } = useTelegram();
|
||||
@@ -104,17 +124,17 @@ export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
// Fetch HEZ balance from Asset Hub (native token)
|
||||
|
||||
const hezAccount = (await withTimeout(
|
||||
(assetHubApi.query.system as any).account(keypair.address),
|
||||
assetHubApi.query.system.account(keypair.address),
|
||||
10000
|
||||
)) as any;
|
||||
)) as unknown as AccountInfoResult;
|
||||
if (isCancelled) return;
|
||||
const hezFree = hezAccount.data.free.toString();
|
||||
setBalances((prev) => ({ ...prev, HEZ: (parseInt(hezFree) / 1e12).toFixed(4) }));
|
||||
|
||||
const pezResult = (await withTimeout(
|
||||
(assetHubApi.query.assets as any).account(1, keypair.address),
|
||||
assetHubApi.query.assets.account(1, keypair.address),
|
||||
10000
|
||||
)) as any;
|
||||
)) as unknown as AssetAccountResult;
|
||||
if (isCancelled) return;
|
||||
if (pezResult.isSome) {
|
||||
setBalances((prev) => ({
|
||||
@@ -126,9 +146,9 @@ export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
}
|
||||
|
||||
const usdtResult = (await withTimeout(
|
||||
(assetHubApi.query.assets as any).account(1000, keypair.address),
|
||||
assetHubApi.query.assets.account(1000, keypair.address),
|
||||
10000
|
||||
)) as any;
|
||||
)) as unknown as AssetAccountResult;
|
||||
if (isCancelled) return;
|
||||
if (usdtResult.isSome) {
|
||||
setBalances((prev) => ({
|
||||
@@ -175,7 +195,9 @@ export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
const oneUnit = BigInt(Math.pow(10, token0.decimals));
|
||||
|
||||
const quote = await withTimeout(
|
||||
(assetHubApi.call as any).assetConversionApi.quotePriceExactTokensForTokens(
|
||||
(
|
||||
assetHubApi.call as unknown as AssetConversionCall
|
||||
).assetConversionApi.quotePriceExactTokensForTokens(
|
||||
formatAssetLocation(asset0),
|
||||
formatAssetLocation(asset1),
|
||||
oneUnit.toString(),
|
||||
@@ -184,11 +206,8 @@ export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
10000
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (quote && !(quote as any).isNone) {
|
||||
price =
|
||||
Number(BigInt((quote as any).unwrap().toString())) /
|
||||
Math.pow(10, token1.decimals);
|
||||
if (quote && !quote.isNone) {
|
||||
price = Number(BigInt(quote.unwrap().toString())) / Math.pow(10, token1.decimals);
|
||||
|
||||
// Estimate reserves from LP supply
|
||||
const lpAsset = await withTimeout(
|
||||
@@ -283,6 +302,10 @@ export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
// `t` is intentionally omitted: it is only used for error messages and its
|
||||
// identity changes only on language switch — including it would re-run the
|
||||
// blockchain fetch (extra RPC calls) on every language change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, assetHubApi, keypair]);
|
||||
|
||||
// Auto-calculate amount1 based on pool price
|
||||
@@ -326,28 +349,36 @@ export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
keypair.address
|
||||
);
|
||||
|
||||
// Minimal shape of the signAndSend result fields we read. `asModule`
|
||||
// is typed from the registry call so it stays type-safe.
|
||||
type TxResult = {
|
||||
status: { isFinalized: boolean };
|
||||
dispatchError?: {
|
||||
isModule: boolean;
|
||||
asModule: Parameters<typeof assetHubApi.registry.findMetaError>[0];
|
||||
toString(): string;
|
||||
};
|
||||
};
|
||||
|
||||
// Wait for transaction to be finalized
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
tx.signAndSend(
|
||||
keypair,
|
||||
({ status, dispatchError }: { status: any; dispatchError: any }) => {
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMsg = t('pools.addFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else if (dispatchError.toString) {
|
||||
errorMsg = dispatchError.toString();
|
||||
}
|
||||
console.error('Add liquidity error:', errorMsg);
|
||||
reject(new Error(errorMsg));
|
||||
} else {
|
||||
resolve();
|
||||
tx.signAndSend(keypair, ({ status, dispatchError }: TxResult) => {
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMsg = t('pools.addFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else if (dispatchError.toString) {
|
||||
errorMsg = dispatchError.toString();
|
||||
}
|
||||
console.error('Add liquidity error:', errorMsg);
|
||||
reject(new Error(errorMsg));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
).catch(reject);
|
||||
}).catch(reject);
|
||||
});
|
||||
|
||||
setSuccessMessage(
|
||||
@@ -420,28 +451,36 @@ export function PoolsModal({ isOpen, onClose }: PoolsModalProps) {
|
||||
keypair.address
|
||||
);
|
||||
|
||||
// Minimal shape of the signAndSend result fields we read. `asModule`
|
||||
// is typed from the registry call so it stays type-safe.
|
||||
type TxResult = {
|
||||
status: { isFinalized: boolean };
|
||||
dispatchError?: {
|
||||
isModule: boolean;
|
||||
asModule: Parameters<typeof assetHubApi.registry.findMetaError>[0];
|
||||
toString(): string;
|
||||
};
|
||||
};
|
||||
|
||||
// Wait for transaction to be finalized
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
tx.signAndSend(
|
||||
keypair,
|
||||
({ status, dispatchError }: { status: any; dispatchError: any }) => {
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMsg = t('pools.removeFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else if (dispatchError.toString) {
|
||||
errorMsg = dispatchError.toString();
|
||||
}
|
||||
console.error('Remove liquidity error:', errorMsg);
|
||||
reject(new Error(errorMsg));
|
||||
} else {
|
||||
resolve();
|
||||
tx.signAndSend(keypair, ({ status, dispatchError }: TxResult) => {
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMsg = t('pools.removeFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else if (dispatchError.toString) {
|
||||
errorMsg = dispatchError.toString();
|
||||
}
|
||||
console.error('Remove liquidity error:', errorMsg);
|
||||
reject(new Error(errorMsg));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
).catch(reject);
|
||||
}).catch(reject);
|
||||
});
|
||||
|
||||
setSuccessMessage(t('pools.removedLiquidity', { amount: lpAmountToRemove }));
|
||||
|
||||
@@ -74,10 +74,25 @@ export function SwapModal({ isOpen, onClose }: SwapModalProps) {
|
||||
const hezFree = hezAccount.data.free.toString();
|
||||
const hezBalance = (parseInt(hezFree) / 1e12).toFixed(4);
|
||||
|
||||
// Helper to extract balance from asset query result
|
||||
const getAssetBalance = (result: any, decimals: number, fractionDigits: number): string => {
|
||||
// Helper to extract balance from an asset storage query result. The
|
||||
// shape is the subset of the @pezkuwi/api Option/Codec result we read.
|
||||
type AssetQueryResult = {
|
||||
isEmpty: boolean;
|
||||
isSome?: boolean;
|
||||
unwrap?: () => { toJSON(): unknown };
|
||||
toJSON(): unknown;
|
||||
};
|
||||
const getAssetBalance = (
|
||||
result: AssetQueryResult | null,
|
||||
decimals: number,
|
||||
fractionDigits: number
|
||||
): string => {
|
||||
if (!result || result.isEmpty) return '0'.padEnd(fractionDigits + 2, '0');
|
||||
const data = result.isSome ? result.unwrap().toJSON() : result.toJSON();
|
||||
const data = (
|
||||
result.isSome && result.unwrap ? result.unwrap().toJSON() : result.toJSON()
|
||||
) as {
|
||||
balance?: { toString(): string };
|
||||
} | null;
|
||||
if (data && data.balance) {
|
||||
return (parseInt(data.balance.toString()) / Math.pow(10, decimals)).toFixed(
|
||||
fractionDigits
|
||||
@@ -155,8 +170,21 @@ export function SwapModal({ isOpen, onClose }: SwapModalProps) {
|
||||
const decimals2 = getDecimals(asset2);
|
||||
const oneUnit = BigInt(Math.pow(10, decimals1));
|
||||
|
||||
// assetConversionApi is a runtime API not present in the base
|
||||
// @pezkuwi/api typings; describe just the call we use.
|
||||
type QuoteResult = { isNone: boolean; unwrap(): { toString(): string } };
|
||||
type AssetConversionCall = {
|
||||
assetConversionApi: {
|
||||
quotePriceExactTokensForTokens: (
|
||||
asset1: ReturnType<typeof formatAssetLocation>,
|
||||
asset2: ReturnType<typeof formatAssetLocation>,
|
||||
amount: string,
|
||||
includeFee: boolean
|
||||
) => Promise<QuoteResult>;
|
||||
};
|
||||
};
|
||||
const quote = await (
|
||||
assetHubApi.call as any
|
||||
assetHubApi.call as unknown as AssetConversionCall
|
||||
).assetConversionApi.quotePriceExactTokensForTokens(
|
||||
formatAssetLocation(asset1),
|
||||
formatAssetLocation(asset2),
|
||||
@@ -257,28 +285,38 @@ export function SwapModal({ isOpen, onClose }: SwapModalProps) {
|
||||
true
|
||||
);
|
||||
|
||||
// Minimal shape of the signAndSend result fields we read (the full
|
||||
// @pezkuwi/api ISubmittableResult is unavailable because `tx` above is
|
||||
// produced from an untyped runtime extrinsic).
|
||||
type SwapDispatchError = {
|
||||
isModule: boolean;
|
||||
asModule: Parameters<typeof assetHubApi.registry.findMetaError>[0];
|
||||
toString(): string;
|
||||
};
|
||||
type SwapTxResult = {
|
||||
status: { isFinalized: boolean };
|
||||
dispatchError?: SwapDispatchError;
|
||||
};
|
||||
|
||||
// Wait for transaction to be finalized
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
tx.signAndSend(
|
||||
keypair,
|
||||
({ status, dispatchError }: { status: any; dispatchError: any }) => {
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMsg = t('swap.swapFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else if (dispatchError.toString) {
|
||||
errorMsg = dispatchError.toString();
|
||||
}
|
||||
console.error('Swap error:', errorMsg);
|
||||
reject(new Error(errorMsg));
|
||||
} else {
|
||||
resolve();
|
||||
tx.signAndSend(keypair, ({ status, dispatchError }: SwapTxResult) => {
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMsg = t('swap.swapFailed');
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else if (dispatchError.toString) {
|
||||
errorMsg = dispatchError.toString();
|
||||
}
|
||||
console.error('Swap error:', errorMsg);
|
||||
reject(new Error(errorMsg));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
).catch(reject);
|
||||
}).catch(reject);
|
||||
});
|
||||
|
||||
setSuccess(true);
|
||||
|
||||
@@ -21,11 +21,14 @@ import {
|
||||
TrendingDown,
|
||||
Fuel,
|
||||
} from 'lucide-react';
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
import { useTranslation } from '@/i18n';
|
||||
import {
|
||||
subscribeToConnection,
|
||||
subscribeToAssetHubConnection,
|
||||
subscribeToPeopleConnection,
|
||||
getLastError,
|
||||
getAssetHubAPI,
|
||||
getPeopleAPI,
|
||||
@@ -76,6 +79,7 @@ interface TokenConfig {
|
||||
logo: string;
|
||||
isDefault: boolean;
|
||||
priority: number; // Lower = higher in list
|
||||
standard?: 'PEZ-20'; // fungible Asset Hub asset → PEZ-20 token standard
|
||||
}
|
||||
|
||||
const DEFAULT_TOKENS: TokenConfig[] = [
|
||||
@@ -98,6 +102,7 @@ const DEFAULT_TOKENS: TokenConfig[] = [
|
||||
logo: '/tokens/PEZ.png',
|
||||
isDefault: true,
|
||||
priority: 1,
|
||||
standard: 'PEZ-20',
|
||||
},
|
||||
{
|
||||
assetId: ASSET_IDS.WUSDT,
|
||||
@@ -108,6 +113,7 @@ const DEFAULT_TOKENS: TokenConfig[] = [
|
||||
logo: '/tokens/USDT.png',
|
||||
isDefault: true,
|
||||
priority: 2,
|
||||
standard: 'PEZ-20',
|
||||
},
|
||||
{
|
||||
assetId: ASSET_IDS.DOT,
|
||||
@@ -209,51 +215,68 @@ export function TokensCard({ onSendToken }: Props) {
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
// Fetch multi-chain HEZ balances (Asset Hub & People Chain)
|
||||
// Live multi-chain HEZ balances (Asset Hub & People Chain).
|
||||
// Uses real-time storage subscriptions and (re)subscribes the moment each
|
||||
// chain connects — so balances populate as soon as the chain is ready and
|
||||
// update instantly on any change (no 30s polling lag, no stuck "--").
|
||||
useEffect(() => {
|
||||
if (!address) return;
|
||||
let cancelled = false;
|
||||
let ahBalUnsub: (() => void) | null = null;
|
||||
let peopleBalUnsub: (() => void) | null = null;
|
||||
|
||||
const fetchMultiChainBalances = async () => {
|
||||
// Asset Hub HEZ balance
|
||||
const assetHubApi = getAssetHubAPI();
|
||||
if (assetHubApi) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const accountInfo = (await (assetHubApi.query.system as any).account(address)) as {
|
||||
data: { free: { toString(): string } };
|
||||
};
|
||||
const free = accountInfo.data.free.toString();
|
||||
const balanceNum = Number(free) / 1e12;
|
||||
setAssetHubHezBalance(balanceNum.toFixed(4));
|
||||
} catch (err) {
|
||||
console.error('Error fetching Asset Hub HEZ balance:', err);
|
||||
setAssetHubHezBalance('0.0000');
|
||||
}
|
||||
}
|
||||
|
||||
// People Chain HEZ balance
|
||||
const peopleApi = getPeopleAPI();
|
||||
if (peopleApi) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const accountInfo = (await (peopleApi.query.system as any).account(address)) as {
|
||||
data: { free: { toString(): string } };
|
||||
};
|
||||
const free = accountInfo.data.free.toString();
|
||||
const balanceNum = Number(free) / 1e12;
|
||||
setPeopleHezBalance(balanceNum.toFixed(4));
|
||||
} catch (err) {
|
||||
console.error('Error fetching People Chain HEZ balance:', err);
|
||||
setPeopleHezBalance('0.0000');
|
||||
}
|
||||
type AccountInfo = { data: { free: { toString(): string } } };
|
||||
const liveBalance = async (
|
||||
api: ApiPromise | null,
|
||||
setBalance: (v: string) => void,
|
||||
label: string
|
||||
) => {
|
||||
if (!api) return null;
|
||||
try {
|
||||
// callback form = live subscription, fires on every change
|
||||
const unsub = await api.query.system.account(address, (info: AccountInfo) => {
|
||||
const balanceNum = Number(info.data.free.toString()) / 1e12;
|
||||
setBalance(balanceNum.toFixed(4));
|
||||
});
|
||||
return unsub as unknown as () => void;
|
||||
} catch (err) {
|
||||
console.error(`Error subscribing to ${label} HEZ balance:`, err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
fetchMultiChainBalances();
|
||||
// Refresh every 30 seconds
|
||||
const interval = setInterval(fetchMultiChainBalances, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [address, rpcConnected]);
|
||||
const unsubAhConn = subscribeToAssetHubConnection(async (connected) => {
|
||||
if (ahBalUnsub) {
|
||||
ahBalUnsub();
|
||||
ahBalUnsub = null;
|
||||
}
|
||||
if (connected) {
|
||||
const u = await liveBalance(getAssetHubAPI(), setAssetHubHezBalance, 'Asset Hub');
|
||||
if (cancelled) u?.();
|
||||
else ahBalUnsub = u;
|
||||
}
|
||||
});
|
||||
|
||||
const unsubPeopleConn = subscribeToPeopleConnection(async (connected) => {
|
||||
if (peopleBalUnsub) {
|
||||
peopleBalUnsub();
|
||||
peopleBalUnsub = null;
|
||||
}
|
||||
if (connected) {
|
||||
const u = await liveBalance(getPeopleAPI(), setPeopleHezBalance, 'People Chain');
|
||||
if (cancelled) u?.();
|
||||
else peopleBalUnsub = u;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (ahBalUnsub) ahBalUnsub();
|
||||
if (peopleBalUnsub) peopleBalUnsub();
|
||||
unsubAhConn();
|
||||
unsubPeopleConn();
|
||||
};
|
||||
}, [address]);
|
||||
|
||||
// Initialize with default tokens immediately (no API required)
|
||||
const [tokens, setTokens] = useState<TokenBalance[]>(() =>
|
||||
@@ -838,6 +861,18 @@ export function TokensCard({ onSendToken }: Props) {
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold">{token.displaySymbol}</span>
|
||||
{token.standard === 'PEZ-20' && (
|
||||
<a
|
||||
href="https://docs.pezkuwichain.io/token-standards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="PEZ-20 token standard on Pezkuwi Asset Hub"
|
||||
className="text-[10px] bg-blue-500/20 text-blue-300 px-1.5 py-0.5 rounded no-underline"
|
||||
>
|
||||
PEZ-20
|
||||
</a>
|
||||
)}
|
||||
{token.assetId <= -100 && (
|
||||
<span className="text-[10px] bg-purple-500/20 text-purple-400 px-1.5 py-0.5 rounded">
|
||||
LP
|
||||
|
||||
@@ -1103,14 +1103,23 @@ function SendTab({ onBack }: { onBack: () => void }) {
|
||||
let tx;
|
||||
|
||||
if (selectedToken === 'HEZ') {
|
||||
// HEZ transfer on main chain
|
||||
// HEZ transfer on main chain (api guaranteed by the guard above, but
|
||||
// narrow again here so TS knows it is non-null in this branch).
|
||||
if (!api) {
|
||||
setError(t('send.mainnetApiNotReady'));
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tx = (api!.tx.balances as any).transferKeepAlive(toAddress, amountInSmallestUnit);
|
||||
tx = (api.tx.balances as any).transferKeepAlive(toAddress, amountInSmallestUnit);
|
||||
} else {
|
||||
// Asset transfer on Asset Hub (PEZ: asset ID 1, USDT: asset ID 1000)
|
||||
if (!assetHubApi) {
|
||||
setError(t('send.assetHubApiNotReady'));
|
||||
return;
|
||||
}
|
||||
const assetId = tokenInfo?.assetId ?? 1;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tx = (assetHubApi!.tx.assets as any).transfer(assetId, toAddress, amountInSmallestUnit);
|
||||
tx = (assetHubApi.tx.assets as any).transfer(assetId, toAddress, amountInSmallestUnit);
|
||||
}
|
||||
|
||||
const hash = await tx.signAndSend(keypair);
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
subscribeToReferralEvents,
|
||||
type ReferralStats,
|
||||
} from '@/lib/referral';
|
||||
import { getPendingApprovals, getCitizenshipStatus, type PendingApproval } from '@/lib/citizenship';
|
||||
|
||||
interface ReferralContextValue {
|
||||
stats: ReferralStats | null;
|
||||
myReferrals: string[];
|
||||
pendingApprovals: PendingApproval[];
|
||||
loading: boolean;
|
||||
refreshStats: () => Promise<void>;
|
||||
}
|
||||
@@ -31,6 +33,7 @@ export function ReferralProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const [stats, setStats] = useState<ReferralStats | null>(null);
|
||||
const [myReferrals, setMyReferrals] = useState<string[]>([]);
|
||||
const [pendingApprovals, setPendingApprovals] = useState<PendingApproval[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch referral statistics from People Chain
|
||||
@@ -38,6 +41,7 @@ export function ReferralProvider({ children }: { children: ReactNode }) {
|
||||
if (!peopleApi || !address) {
|
||||
setStats(null);
|
||||
setMyReferrals([]);
|
||||
setPendingApprovals([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -52,6 +56,15 @@ export function ReferralProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
setStats(fetchedStats);
|
||||
setMyReferrals(fetchedReferrals);
|
||||
|
||||
// Fetch pending approvals only if user is an approved citizen (can be a referrer)
|
||||
const citizenStatus = await getCitizenshipStatus(peopleApi, address);
|
||||
if (citizenStatus === 'Approved') {
|
||||
const approvals = await getPendingApprovals(peopleApi, address);
|
||||
setPendingApprovals(approvals);
|
||||
} else {
|
||||
setPendingApprovals([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching referral stats:', error);
|
||||
showAlert(translate('context.referralStatsError'));
|
||||
@@ -92,6 +105,7 @@ export function ReferralProvider({ children }: { children: ReactNode }) {
|
||||
const value: ReferralContextValue = {
|
||||
stats,
|
||||
myReferrals,
|
||||
pendingApprovals,
|
||||
loading,
|
||||
refreshStats: fetchStats,
|
||||
};
|
||||
|
||||
@@ -171,6 +171,12 @@ const ar: Translations = {
|
||||
noUnclaimedRewards: 'لا توجد مكافآت غير مطالب بها',
|
||||
rewardHistory: 'سجل المكافآت',
|
||||
era: 'حقبة',
|
||||
pendingApprovals: 'الموافقات المعلّقة',
|
||||
approveReferral: 'موافقة',
|
||||
approvingReferral: 'جاري الموافقة...',
|
||||
referralApprovalSuccess: 'تمت الموافقة على الإحالة!',
|
||||
referralApprovalFailed: 'فشلت الموافقة',
|
||||
pendingReferralStatus: 'بانتظار موافقتك',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
@@ -826,6 +832,8 @@ const ar: Translations = {
|
||||
alreadyPending: 'لديك طلب قيد الانتظار',
|
||||
alreadyApproved: 'مواطنتك معتمدة بالفعل!',
|
||||
insufficientBalance: 'رصيد غير كافٍ (١ HEZ وديعة مطلوبة)',
|
||||
applyingCitizenship: 'جاري تقديم طلب المواطنة...',
|
||||
applicationSuccess: 'تم تقديم الطلب!',
|
||||
selectLanguage: 'اختر اللغة',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -172,6 +172,12 @@ const ckb: Translations = {
|
||||
noUnclaimedRewards: 'خەڵاتی داوانەکراو نییە',
|
||||
rewardHistory: 'مێژووی خەڵاتەکان',
|
||||
era: 'سەردەم',
|
||||
pendingApprovals: 'پەسەندکردنە چاوەڕوانەکان',
|
||||
approveReferral: 'پەسەندکردن',
|
||||
approvingReferral: 'پەسەند دەکرێت...',
|
||||
referralApprovalSuccess: 'بانگهێشتکردن پەسەندکرا!',
|
||||
referralApprovalFailed: 'پەسەندکردن سەرنەکەوت',
|
||||
pendingReferralStatus: 'چاوەڕوانی پەسەندکردنی تۆیە',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
@@ -829,6 +835,8 @@ const ckb: Translations = {
|
||||
alreadyPending: 'داواکارییەکی چاوەڕوانت هەیە',
|
||||
alreadyApproved: 'هاوڵاتیبوونت پێشتر پەسەند کراوە!',
|
||||
insufficientBalance: 'باڵانسی پێویست نییە (١ HEZ ئەمانەت پێویستە)',
|
||||
applyingCitizenship: 'داواکاری هاوڵاتیبوون دەنێردرێت...',
|
||||
applicationSuccess: 'داواکاری نێردرا!',
|
||||
selectLanguage: 'زمان هەڵبژێرە',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -171,6 +171,12 @@ const en: Translations = {
|
||||
noUnclaimedRewards: 'No unclaimed rewards',
|
||||
rewardHistory: 'Reward History',
|
||||
era: 'Era',
|
||||
pendingApprovals: 'Pending Approvals',
|
||||
approveReferral: 'Approve',
|
||||
approvingReferral: 'Approving...',
|
||||
referralApprovalSuccess: 'Referral approved!',
|
||||
referralApprovalFailed: 'Approval failed',
|
||||
pendingReferralStatus: 'Awaiting your approval',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
@@ -829,6 +835,8 @@ const en: Translations = {
|
||||
alreadyPending: 'You already have a pending application',
|
||||
alreadyApproved: 'Your citizenship is already approved!',
|
||||
insufficientBalance: 'Insufficient balance (1 HEZ deposit required)',
|
||||
applyingCitizenship: 'Applying for citizenship...',
|
||||
applicationSuccess: 'Application submitted!',
|
||||
selectLanguage: 'Select language',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -171,6 +171,12 @@ const fa: Translations = {
|
||||
noUnclaimedRewards: 'پاداش مطالبه نشدهای وجود ندارد',
|
||||
rewardHistory: 'تاریخچه پاداشها',
|
||||
era: 'دوره',
|
||||
pendingApprovals: 'تأییدهای در انتظار',
|
||||
approveReferral: 'تأیید',
|
||||
approvingReferral: 'در حال تأیید...',
|
||||
referralApprovalSuccess: 'دعوت تأیید شد!',
|
||||
referralApprovalFailed: 'تأیید ناموفق بود',
|
||||
pendingReferralStatus: 'در انتظار تأیید شما',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
@@ -829,6 +835,8 @@ const fa: Translations = {
|
||||
alreadyPending: 'درخواست در انتظار دارید',
|
||||
alreadyApproved: 'شهروندی شما قبلاً تأیید شده!',
|
||||
insufficientBalance: 'موجودی ناکافی (۱ HEZ سپرده لازم است)',
|
||||
applyingCitizenship: 'در حال ارسال درخواست شهروندی...',
|
||||
applicationSuccess: 'درخواست ارسال شد!',
|
||||
selectLanguage: 'انتخاب زبان',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -176,6 +176,12 @@ const krd: Translations = {
|
||||
noUnclaimedRewards: 'Xelatên nedaxwazkir tune ne',
|
||||
rewardHistory: 'Dîroka Xelatan',
|
||||
era: 'Era',
|
||||
pendingApprovals: 'Pejirandina li bendê',
|
||||
approveReferral: 'Pejirîne',
|
||||
approvingReferral: 'Tê pejirandin...',
|
||||
referralApprovalSuccess: 'Referans hat pejirandin!',
|
||||
referralApprovalFailed: 'Pejirandin biserneket',
|
||||
pendingReferralStatus: 'Li benda pejirandina we',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
@@ -856,6 +862,8 @@ const krd: Translations = {
|
||||
alreadyPending: 'Serlêdanek te ya li bendê heye',
|
||||
alreadyApproved: 'Welatîbûna te berê hatiye pejirandin!',
|
||||
insufficientBalance: 'Balansa têr nîne (1 HEZ depozîto pêwîst e)',
|
||||
applyingCitizenship: 'Daxwaza welatîbûnê tê şandin...',
|
||||
applicationSuccess: 'Daxwaz hat şandin!',
|
||||
selectLanguage: 'Ziman hilbijêre',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -171,6 +171,12 @@ const tr: Translations = {
|
||||
noUnclaimedRewards: 'Talep edilmemiş ödül yok',
|
||||
rewardHistory: 'Ödül Geçmişi',
|
||||
era: 'Era',
|
||||
pendingApprovals: 'Bekleyen Onaylar',
|
||||
approveReferral: 'Onayla',
|
||||
approvingReferral: 'Onaylanıyor...',
|
||||
referralApprovalSuccess: 'Referans onaylandı!',
|
||||
referralApprovalFailed: 'Onaylama başarısız',
|
||||
pendingReferralStatus: 'Onayınızı bekliyor',
|
||||
},
|
||||
|
||||
wallet: {
|
||||
@@ -829,6 +835,8 @@ const tr: Translations = {
|
||||
alreadyPending: 'Bekleyen bir başvurunuz var',
|
||||
alreadyApproved: 'Vatandaşlığınız zaten onaylanmış!',
|
||||
insufficientBalance: 'Yetersiz bakiye (1 HEZ depozito gerekli)',
|
||||
applyingCitizenship: 'Vatandaşlık başvurusu yapılıyor...',
|
||||
applicationSuccess: 'Başvuru gönderildi!',
|
||||
selectLanguage: 'Dil seçin',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -173,6 +173,12 @@ export interface Translations {
|
||||
noUnclaimedRewards: string;
|
||||
rewardHistory: string;
|
||||
era: string;
|
||||
pendingApprovals: string;
|
||||
approveReferral: string;
|
||||
approvingReferral: string;
|
||||
referralApprovalSuccess: string;
|
||||
referralApprovalFailed: string;
|
||||
pendingReferralStatus: string;
|
||||
};
|
||||
|
||||
// Wallet section
|
||||
@@ -851,6 +857,9 @@ export interface Translations {
|
||||
alreadyPending: string;
|
||||
alreadyApproved: string;
|
||||
insufficientBalance: string;
|
||||
// In-app modal
|
||||
applyingCitizenship: string;
|
||||
applicationSuccess: string;
|
||||
// Language selector
|
||||
selectLanguage: string;
|
||||
};
|
||||
|
||||
@@ -46,6 +46,11 @@ export interface CitizenshipResult {
|
||||
identityHash?: string;
|
||||
}
|
||||
|
||||
export interface PendingApproval {
|
||||
applicantAddress: string;
|
||||
identityHash: string;
|
||||
}
|
||||
|
||||
// ── Identity Hash (Keccak-256) ──────────────────────────────────────
|
||||
|
||||
export function calculateIdentityHash(name: string, email: string, documentCids: string[]): string {
|
||||
@@ -140,6 +145,128 @@ export async function getCitizenshipStatus(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pending Approvals ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get pending referral applications that need approval from the given referrer.
|
||||
* Queries identityKyc.applications entries and filters by referrer + PendingReferral status.
|
||||
*/
|
||||
export async function getPendingApprovals(
|
||||
api: ApiPromise,
|
||||
referrerAddress: string
|
||||
): Promise<PendingApproval[]> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!(api?.query as any)?.identityKyc?.applications) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const entries = await (api.query as any).identityKyc.applications.entries();
|
||||
const pending: PendingApproval[] = [];
|
||||
|
||||
// Convert referrer SS58 address to raw hex account ID for comparison
|
||||
// Application value is SCALE-encoded: identity_hash (32 bytes) + referrer (32 bytes)
|
||||
const referrerAccountId = api
|
||||
.createType('AccountId32', referrerAddress)
|
||||
.toHex()
|
||||
.replace('0x', '')
|
||||
.toLowerCase();
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
const applicantAddress = key.args[0].toString();
|
||||
|
||||
// Extract referrer from raw hex (last 32 bytes = 64 hex chars)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rawHex = (value as any).toHex().replace('0x', '');
|
||||
if (rawHex.length < 128) continue; // need at least 64 bytes
|
||||
|
||||
const identityHashHex = '0x' + rawHex.slice(0, 64);
|
||||
const referrerHex = rawHex.slice(64, 128).toLowerCase();
|
||||
|
||||
// Compare raw account IDs
|
||||
if (referrerHex !== referrerAccountId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if status is PendingReferral
|
||||
const status = await getCitizenshipStatus(api, applicantAddress);
|
||||
if (status === 'PendingReferral') {
|
||||
pending.push({
|
||||
applicantAddress,
|
||||
identityHash: identityHashHex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return pending;
|
||||
} catch (error) {
|
||||
console.error('[Citizenship] Error fetching pending approvals:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Approve Referral ────────────────────────────────────────────────
|
||||
|
||||
export async function approveReferral(
|
||||
api: ApiPromise,
|
||||
keypair: KeyringPair,
|
||||
applicantAddress: string
|
||||
): Promise<CitizenshipResult> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tx = api.tx as any;
|
||||
if (!tx?.identityKyc?.approveReferral) {
|
||||
return { success: false, error: 'Identity KYC pallet not available' };
|
||||
}
|
||||
|
||||
const result = await new Promise<CitizenshipResult>((resolve) => {
|
||||
tx.identityKyc
|
||||
.approveReferral(applicantAddress)
|
||||
.signAndSend(
|
||||
keypair,
|
||||
{ nonce: -1 },
|
||||
({
|
||||
status,
|
||||
dispatchError,
|
||||
}: {
|
||||
status: {
|
||||
isInBlock: boolean;
|
||||
isFinalized: boolean;
|
||||
asInBlock?: { toString: () => string };
|
||||
asFinalized?: { toString: () => string };
|
||||
};
|
||||
dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string };
|
||||
}) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Referral approval failed';
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(
|
||||
dispatchError.asModule as Parameters<typeof api.registry.findMetaError>[0]
|
||||
);
|
||||
errorMessage = `${decoded.section}.${decoded.name}`;
|
||||
}
|
||||
resolve({ success: false, error: errorMessage });
|
||||
return;
|
||||
}
|
||||
const blockHash = status.asFinalized?.toString() || status.asInBlock?.toString();
|
||||
resolve({ success: true, blockHash });
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch((error: Error) => resolve({ success: false, error: error.message }));
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Blockchain Submission ───────────────────────────────────────────
|
||||
|
||||
export async function applyCitizenship(
|
||||
|
||||
+3
-1
@@ -73,7 +73,9 @@ export async function getPendingReferral(api: ApiPromise, address: string): Prom
|
||||
try {
|
||||
// Check if referral pallet exists
|
||||
if (!isReferralPalletAvailable(api)) {
|
||||
console.log('Referral pallet not available on this chain');
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('Referral pallet not available on this chain');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+15
-9
@@ -306,27 +306,33 @@ const TIKI_NAME_SCORES: Record<string, number> = {
|
||||
* Storage: tiki.userTikis(address) -> Vec<TikiRole>
|
||||
*/
|
||||
export async function fetchUserTikis(peopleApi: ApiPromise, address: string): Promise<TikiInfo[]> {
|
||||
// The `tiki` pallet is not part of the base @pezkuwi/api typings, so we
|
||||
// describe just the storage entries we read here.
|
||||
type TikiStorageResult = { isEmpty: boolean; toJSON(): unknown };
|
||||
type TikiQuery = {
|
||||
userTikis?: (address: string) => Promise<TikiStorageResult>;
|
||||
userRoles?: (address: string) => Promise<TikiStorageResult>;
|
||||
};
|
||||
type TikiEntry = string | { name?: string; role?: string };
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (!(peopleApi?.query as any)?.tiki) {
|
||||
const tikiQuery = (peopleApi?.query as { tiki?: TikiQuery } | undefined)?.tiki;
|
||||
if (!tikiQuery) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let result = await (peopleApi.query.tiki as any).userTikis?.(address);
|
||||
let result = await tikiQuery.userTikis?.(address);
|
||||
|
||||
// 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 && tikiQuery.userRoles) {
|
||||
result = await tikiQuery.userRoles?.(address);
|
||||
}
|
||||
|
||||
if (!result || result.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tikis = result.toJSON() as any[];
|
||||
const tikis = result.toJSON() as TikiEntry[];
|
||||
|
||||
return tikis.map((tiki, index) => {
|
||||
const name = typeof tiki === 'string' ? tiki : tiki.name || tiki.role || 'Unknown';
|
||||
|
||||
+8
-5
@@ -9,13 +9,16 @@ import { LanguageProvider } from './i18n';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
// Suppress console logs in production
|
||||
// Suppress non-critical console output in production. This is the one place
|
||||
// that legitimately overrides console.log/debug/info — warn/error are kept for
|
||||
// critical issues. The no-console rule is disabled only for these assignments.
|
||||
if (import.meta.env.PROD) {
|
||||
const noop = () => {};
|
||||
console.log = noop;
|
||||
console.debug = noop;
|
||||
console.info = noop;
|
||||
// Keep console.warn and console.error for critical issues
|
||||
const suppressed: Array<'log' | 'debug' | 'info'> = ['log', 'debug', 'info'];
|
||||
for (const method of suppressed) {
|
||||
// eslint-disable-next-line no-console
|
||||
console[method] = noop;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Telegram WebApp
|
||||
|
||||
@@ -24,10 +24,12 @@ import {
|
||||
Play,
|
||||
PenTool,
|
||||
Globe2,
|
||||
Clock,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { cn, formatAddress } from '@/lib/utils';
|
||||
import { useTelegram } from '@/hooks/useTelegram';
|
||||
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useReferral } from '@/contexts/ReferralContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { SocialLinks } from '@/components/SocialLinks';
|
||||
@@ -62,9 +64,11 @@ import {
|
||||
getCitizenshipStatus,
|
||||
getCitizenCount,
|
||||
confirmCitizenship,
|
||||
approveReferral,
|
||||
type CitizenshipStatus,
|
||||
} from '@/lib/citizenship';
|
||||
import { KurdistanSun } from '@/components/KurdistanSun';
|
||||
import { CitizenshipModal } from '@/components/CitizenshipModal';
|
||||
|
||||
// Activity tracking constants
|
||||
const ACTIVITY_STORAGE_KEY = 'pezkuwi_last_active';
|
||||
@@ -72,8 +76,8 @@ const ACTIVITY_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
export function RewardsSection() {
|
||||
const { hapticImpact, hapticNotification, shareUrl, showAlert } = useTelegram();
|
||||
|
||||
const { stats, myReferrals, loading, refreshStats } = useReferral();
|
||||
const { user: authUser } = useAuth();
|
||||
const { stats, myReferrals, pendingApprovals, loading, refreshStats } = useReferral();
|
||||
const { isConnected, address, peopleApi, assetHubApi, keypair } = useWallet();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -95,6 +99,8 @@ export function RewardsSection() {
|
||||
const [unclaimedRewards, setUnclaimedRewards] = useState<UnclaimedRewardsResult | null>(null);
|
||||
const [claimingStaking, setClaimingStaking] = useState(false);
|
||||
const [claimingStakingEra, setClaimingStakingEra] = useState<number | null>(null);
|
||||
const [approvingAddress, setApprovingAddress] = useState<string | null>(null);
|
||||
const [showCitizenshipModal, setShowCitizenshipModal] = useState(false);
|
||||
|
||||
// Check activity status
|
||||
const checkActivityStatus = useCallback(() => {
|
||||
@@ -358,9 +364,31 @@ export function RewardsSection() {
|
||||
showAlert(t('rewards.activatedAlert'));
|
||||
};
|
||||
|
||||
// Citizenship referral link - wallet address in start param for auto-fill
|
||||
const referralLink = address
|
||||
? `https://t.me/pezkuwichainBot?start=${address}`
|
||||
const handleApproveReferral = async (applicantAddress: string) => {
|
||||
if (!peopleApi || !keypair) return;
|
||||
setApprovingAddress(applicantAddress);
|
||||
hapticImpact('medium');
|
||||
try {
|
||||
const result = await approveReferral(peopleApi, keypair, applicantAddress);
|
||||
if (result.success) {
|
||||
hapticNotification('success');
|
||||
showAlert(t('rewards.referralApprovalSuccess'));
|
||||
refreshStats();
|
||||
} else {
|
||||
hapticNotification('error');
|
||||
showAlert(result.error || t('rewards.referralApprovalFailed'));
|
||||
}
|
||||
} catch (err) {
|
||||
hapticNotification('error');
|
||||
showAlert(err instanceof Error ? err.message : t('rewards.referralApprovalFailed'));
|
||||
} finally {
|
||||
setApprovingAddress(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Telegram referral link (for sharing) - use authenticated user ID
|
||||
const referralLink = authUser?.telegram_id
|
||||
? `https://t.me/pezkuwichainBot?start=ref_${authUser.telegram_id}`
|
||||
: 'https://t.me/pezkuwichainBot';
|
||||
|
||||
// Full share message: invitation text + link + wallet address for manual paste
|
||||
@@ -496,7 +524,7 @@ export function RewardsSection() {
|
||||
<button
|
||||
onClick={() => {
|
||||
hapticImpact('medium');
|
||||
window.location.href = `${window.location.origin}/citizens${window.location.hash}`;
|
||||
setShowCitizenshipModal(true);
|
||||
}}
|
||||
className="w-full py-2.5 bg-white/20 hover:bg-white/30 rounded-xl text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
@@ -820,6 +848,53 @@ export function RewardsSection() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Pending Approvals Section */}
|
||||
{pendingApprovals.length > 0 && (
|
||||
<div className="bg-gradient-to-r from-orange-500/20 to-amber-500/20 border border-orange-500/30 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Clock className="w-5 h-5 text-orange-400" />
|
||||
<h3 className="font-semibold text-orange-100">{t('rewards.pendingApprovals')}</h3>
|
||||
<span className="bg-orange-500/30 text-orange-300 text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
{pendingApprovals.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{pendingApprovals.map((approval) => (
|
||||
<div
|
||||
key={approval.applicantAddress}
|
||||
className="flex items-center gap-3 bg-black/20 rounded-lg p-3"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<code className="text-sm text-foreground">
|
||||
{formatAddress(approval.applicantAddress, 8)}
|
||||
</code>
|
||||
<p className="text-xs text-orange-300">
|
||||
{t('rewards.pendingReferralStatus')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleApproveReferral(approval.applicantAddress)}
|
||||
disabled={approvingAddress !== null}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/30 text-orange-200 hover:bg-orange-500/40 transition-all disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{approvingAddress === approval.applicantAddress ? (
|
||||
<>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
{t('rewards.approvingReferral')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
{t('rewards.approveReferral')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
@@ -1302,6 +1377,17 @@ export function RewardsSection() {
|
||||
<p className="text-white/60 text-sm mt-2">{t('rewards.signingBlockchain')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Citizenship Application Modal */}
|
||||
<CitizenshipModal
|
||||
isOpen={showCitizenshipModal}
|
||||
onClose={() => {
|
||||
setShowCitizenshipModal(false);
|
||||
if (peopleApi && address) {
|
||||
getCitizenshipStatus(peopleApi, address).then(setCitizenshipStatus);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.229",
|
||||
"buildTime": "2026-02-27T23:33:39.279Z",
|
||||
"buildNumber": 1772235219280
|
||||
"version": "1.0.239",
|
||||
"buildTime": "2026-07-12T12:07:48.783Z",
|
||||
"buildNumber": 1783858068783
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
|
||||
const ANTHROPIC_API_KEY = Deno.env.get('ANTHROPIC_API_KEY') || '';
|
||||
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') || '';
|
||||
const SERVICE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || '';
|
||||
|
||||
const SYSTEM = `You are the official PezkuwiChain AI Assistant on the news site news.pex.mom. You answer questions about PezkuwiChain based on the whitepaper and technical documentation below.
|
||||
|
||||
RULES:
|
||||
- Answer in the SAME LANGUAGE the user writes in. If they write in Kurdish (Kurmancî), answer in Kurdish. If Turkish, answer in Turkish. If English, answer in English. If Arabic, answer in Arabic. If Persian, answer in Persian.
|
||||
- Be concise — website answers should be short, clear and helpful.
|
||||
- Use plain text, no markdown headers. You can use bold with *text* sparingly.
|
||||
- If you don't know something, say so honestly.
|
||||
- Never make up information not in the whitepaper.
|
||||
- You represent PezkuwiChain officially — be professional and helpful.
|
||||
- Do not discuss other blockchain projects comparatively unless asked.
|
||||
- For technical questions about source code, direct users to: github.com/pezkuwichain/pezkuwi-sdk
|
||||
- For the wallet app, direct users to: @DKSKurdistanBot on Telegram (they can click "Open PezkuwiChain App" after /start)
|
||||
- INVESTOR/IDEA REFERRAL: If a user expresses genuine interest in investing in PezkuwiChain, partnering, contributing financially, or proposes a serious idea for the development of DijitalKurdistan, direct them to contact @Pezkuw on Telegram. Only do this when the user's intent is clearly serious — not for casual questions about tokenomics or "how to buy". Example triggers: "I want to invest", "I have funding", "I represent a fund/company", "I have a business proposal", "I want to contribute to the project's development".
|
||||
|
||||
PEZKUWICHAIN WHITEPAPER v5.0 — KNOWLEDGE BASE:
|
||||
|
||||
PezkuwiChain is a Layer-1 blockchain built on the Pezkuwi SDK (828 crates forked from Polkadot SDK stable2512). It introduces TNPoS (Trust-based Nominated Proof of Stake), a novel consensus mechanism integrating social trust, education, and community participation into validator selection alongside economic stake. Launched mainnet in January 2026.
|
||||
|
||||
ARCHITECTURE — Three-Chain System:
|
||||
1. Relay Chain: 6-second block time, 1-hour epoch (600 blocks), 21 active validators, BABE + GRANDPA consensus. Native token: HEZ.
|
||||
2. Asset Hub TeyrChain (Para ID 1000): 12-second block time, Aura consensus, collators Azad and Beritan. Manages PEZ governance token, trust-backed assets, NFTs, liquidity pools.
|
||||
3. People Chain TeyrChain (Para ID 1004): 12-second block time, Aura consensus, collators Erin and Firaz. Manages identity, citizenship, trust scoring, education, governance.
|
||||
|
||||
Technology: Rust (98.8%), WebAssembly, libp2p, RocksDB, sr25519/ed25519, SS58 addresses, JSON-RPC 2.0.
|
||||
|
||||
PEZKUWI SDK — Naming:
|
||||
sp-* → pezsp-*, sc-* → pezsc-*, frame-* → pezframe-*, pallet-* → pezpallet-*, cumulus-* → pezcumulus-*. Substrate → Bizinikiwi, Parachain → TeyrChain, Westend → Zagros testnet. 7,364 Rust source files, 1,926 dependencies.
|
||||
|
||||
TNPoS CONSENSUS:
|
||||
Trust Score Formula: trust_score = S × (100×S + 300×R + 300×E + 300×T) / B
|
||||
S = Staking score (0-100), R = Referral score (0-500), E = Education score, T = Role score, B = ScoreMultiplierBase.
|
||||
If S = 0, trust score = 0. Social components carry 9x the weight of pure staking.
|
||||
|
||||
Staking Score: Based on HEZ bonded + duration. 1-100 HEZ: 20pts, 101-250: 30pts, 251-750: 40pts, 751+: 50pts. Duration multiplier 1.0x (<1 month) to 2.0x (12+ months).
|
||||
Referral Score: 0 referrals: 0pts. 1-10: count×10. 11-50: 100+(count-10)×5. 51-100: 300+(count-50)×4. 101+: 500 max.
|
||||
Education Score: On-chain courses via pezpallet-perwerde. IPFS-linked content, verified completion.
|
||||
Role Score: Soulbound NFT roles via pezpallet-tiki. 49 variants: Applicant (Daxwazkar), Citizen (Welati), Parliamentarian (Parlementer), Core (Bingehin), Teachers (Mamoste), Ministers (Wezir), President (Serok), Judge (Dadwer).
|
||||
|
||||
Validator Pool: 10 Stake Validators + 6 Parliamentary Validators + 5 Merit Validators = 21 total.
|
||||
|
||||
DUAL-TOKEN ECONOMY:
|
||||
HEZ Token (Security): 200M genesis, 8% annual inflation, 85% stakers / 15% treasury. 12 decimals (TYR base unit).
|
||||
Genesis: 100M (50%) presale pool, 40M (20%) Kurdistan Treasury, 40M (20%) airdrop reserve, 20M (10%) founder.
|
||||
Fee: 80% treasury, 20% block author. Tips: 100% block author.
|
||||
|
||||
PEZ Token (Governance): 5 billion fixed supply, 96.25% treasury, 48-month halving.
|
||||
Genesis: 4,812,500,000 PEZ (96.25%) treasury, 93,750,000 PEZ (1.875%) presale, 93,750,000 PEZ (1.875%) founder.
|
||||
Halving: Cycle 1 (2026-2030): 100%, Cycle 2: 50%, Cycle 3: 25%, Cycle 4: 12.5%, Cycle 5: 6.25%.
|
||||
PEZ Rewards: Monthly epochs (432,000 blocks). user_reward = (user_trust_score / total_trust_scores) × epoch_reward_pool.
|
||||
|
||||
Presale: 2% tx fee: 50% treasury, 25% burned, 25% staking rewards.
|
||||
|
||||
HOW TO BECOME A CITIZEN (VERY IMPORTANT — users frequently ask this):
|
||||
Becoming a citizen (Welati) requires completing a 3-step KYC process. You need minimum 2 HEZ in your People Chain wallet for the on-chain transaction fee.
|
||||
|
||||
Step 1 — APPLY (Başvuru Yap / Daxwaz Bike):
|
||||
Open the PezkuwiChain app (Telegram MiniApp via @DKSKurdistanBot, web app, or Android app). Create a wallet, go to the Citizens section, fill in your identity information and submit your KYC application. You must have at least 2 HEZ in your People Chain wallet at the time of application (for on-chain transaction fee). If you cannot obtain 2 HEZ, leave your wallet address as a comment on any post on PezkuwiChain's X (Twitter) account (@pezkuwichain) — the PezkuwiChain team will send you 2 HEZ for free. Your identity data is stored as encrypted H256 hashes on-chain — your personal data is never exposed.
|
||||
|
||||
Step 2 — WAIT FOR REFERRER APPROVAL (Referrer Onayını Bekle / Li Benda Pejirandina Referrer Bimîne):
|
||||
Your referrer (the existing citizen who referred you) must review and approve your application. The referrer validates your identity off-chain and confirms on-chain. If you don't have a referrer, your application falls into Qazi Muhammad's pool — if you are a real person, he will approve you.
|
||||
|
||||
Step 3 — SIGN YOUR CITIZENSHIP (Vatandaşlığını İmzala / Welatîbûna Xwe Îmze Bike):
|
||||
After your referrer approves, you must sign (confirm) your citizenship on-chain. Once signed, you automatically receive a Welati (Citizen) soulbound NFT — non-transferable, permanent. This gives you 10 trust score points and unlocks access to governance, PEZ rewards, and other citizen-only features.
|
||||
|
||||
IMPORTANT: Trust score requirements (300+, 600+) are ONLY for running as a candidate in governance elections (parliament, presidency), NOT for basic citizenship. Any person can become a citizen through the 3-step KYC process above.
|
||||
|
||||
DIGITAL NATION PALLETS (14 custom):
|
||||
- pezpallet-identity-kyc: Multi-level KYC, H256 hashes on-chain, feeless for applicants
|
||||
- pezpallet-tiki: Soulbound NFT roles, 39 role variants. Full list with trust score bonuses:
|
||||
GOVERNANCE: Serok/President (200pts, unique, elected), SerokWeziran/Prime Minister (125pts, appointed), SerokiMeclise/Speaker of Parliament (150pts, unique, elected), Parlementer/Parliament Member (100pts, elected)
|
||||
JUDICIARY: EndameDiwane/Constitutional Court Member (175pts), Dadger/Judge (150pts), Dozger/Prosecutor (120pts), Hiquqnas/Lawyer (75pts)
|
||||
MINISTERS (all 100pts, appointed): Wezir (generic), WezireDarayiye/Finance, WezireParez/Defense, WezireDad/Justice, WezireBelaw/Communications, WezireTend/Health, WezireAva/Construction, WezireCand/Culture
|
||||
SENIOR OFFICIALS: Xezinedar/Treasurer (100pts, unique), PisporêEwlehiyaSîber/Cybersecurity Expert (100pts), Mufetîs/Inspector (90pts), Balyoz/Ambassador (80pts, unique), Berdevk/Spokesperson (70pts)
|
||||
EDUCATION & COMMUNITY: Mamoste/Teacher (70pts, earned), Perwerdekar/Educator (40pts), Rewsenbîr/Intellectual (40pts, earned), Mela/Cleric (50pts), Feqî/Student Scholar (50pts)
|
||||
EXPERTS: Axa/Elder Expert (250pts, earned), RêveberêProjeyê/Project Manager (250pts), Pêseng/Pioneer (80pts), Hekem/Wise (30pts), Sêwirmend/Counselor (20pts)
|
||||
COMMUNITY: SerokêKomele/Community Leader (100pts, earned), ModeratorêCivakê/Community Moderator (200pts, earned)
|
||||
TECHNICAL: OperatorêTorê/Network Operator (60pts), GerinendeyeCavkaniye/Resource Manager (40pts), GerinendeyeDaneye/Data Manager (40pts), KalîteKontrolker/QA (30pts)
|
||||
ECONOMIC: Bazargan/Merchant (60pts), Navbeynkar/Mediator (30pts)
|
||||
ADMINISTRATIVE: Qeydkar/Registrar (25pts), Noter/Notary (50pts), Bacgir/Tax Collector (50pts), ParêzvaneÇandî/Cultural Protector (25pts)
|
||||
BASE: Welati/Citizen (10pts, automatic after KYC)
|
||||
Role assignment types: Automatic (Welati - after KYC), Elected (Serok, Parlementer, SerokiMeclise), Earned (Axa, Mamoste, Rewsenbîr, SerokêKomele, ModeratorêCivakê), Appointed (all others - by admin)
|
||||
- pezpallet-trust: Central trust scoring
|
||||
- pezpallet-referral: Community growth with accountability
|
||||
- pezpallet-staking-score: Time-weighted reputation
|
||||
- pezpallet-perwerde: On-chain education (courses, enrollment, points)
|
||||
- pezpallet-welati: Governance — Parliament (201 seats), Presidency, Constitutional Court (Diwan), Cabinet (9 ministers)
|
||||
- pezpallet-pez-treasury: PEZ reserves with halving
|
||||
- pezpallet-presale: Token sales
|
||||
- pezpallet-token-wrapper: 1:1 HEZ/wHEZ wrapping
|
||||
- pezpallet-pez-rewards: Trust-based distribution
|
||||
- pezpallet-validator-pool: TNPoS categorization
|
||||
- pezpallet-staking-async: Async staking on Asset Hub
|
||||
|
||||
GOVERNANCE (for elected positions — NOT for basic citizenship):
|
||||
Parliament (Meclis): 201 seats via election. Presidency (Serok): 50%+ required. Constitutional Court (Diwan). Cabinet: 9 ministers.
|
||||
Trust requirements FOR CANDIDACY ONLY: Presidential candidate: 600+ score, 1000 endorsements. Parliamentary candidate: 300+ score, 100 endorsements.
|
||||
These are NOT requirements for becoming a citizen. Any person can become a citizen for free.
|
||||
Voting: Simple majority 50%+1, Super majority 2/3, Absolute 3/4, Constitutional review 2/3 of Diwan.
|
||||
|
||||
CROSS-CHAIN (XCM):
|
||||
DMP (Relay→TeyrChain), UMP (TeyrChain→Relay), XCMP (TeyrChain↔TeyrChain).
|
||||
Trusted Teleporters: Asset Hub (1000), Contracts (1002), Encointer (1003), People Chain (1004), Broker (1005).
|
||||
|
||||
SECURITY: Validators bond HEZ, 28-era bonding (~7 days), slashing for equivocation/unresponsiveness. KYC = Sybil resistance. Rust memory safety, WASM sandboxing, forkless upgrades.
|
||||
|
||||
ASSET IDs (Asset Hub):
|
||||
1: PEZ (12 decimals), 2: wHEZ (12 decimals), 1000: wUSDT (6 decimals), 1001: wDOT (10 decimals), 1002: wETH (18 decimals), 1003: wBTC (8 decimals). Native: HEZ (12 decimals).
|
||||
|
||||
RPC Endpoints:
|
||||
Relay Chain: wss://rpc.pezkuwichain.io
|
||||
Asset Hub: wss://asset-hub-rpc.pezkuwichain.io
|
||||
People Chain: wss://people-rpc.pezkuwichain.io
|
||||
|
||||
LINKS:
|
||||
Website: pezkuwichain.io
|
||||
GitHub: github.com/pezkuwichain/pezkuwi-sdk
|
||||
Discord: discord.gg/Y3VyEC6h8W
|
||||
Telegram Channel: t.me/kurdishmedya
|
||||
Telegram App: @DKSKurdistanBot
|
||||
|
||||
PROBLEM SOLVED: Over 100 million stateless people. Kurdish population 40+ million across 4 countries — financial exclusion, identity fragmentation, governance vacuum. PezkuwiChain provides digital nation-state infrastructure.
|
||||
|
||||
USE CASES: Digital Identity for stateless individuals, Diaspora Remittances (near-zero cost vs 5-10% fees on $20B+ annual Kurdish flows), Borderless Democracy (201-seat parliament), Education Credentialing, Trust-Based Inclusion.
|
||||
|
||||
ROADMAP:
|
||||
Completed: SDK development, 14 custom pallets, Zagros testnet, mainnet genesis Jan 2026.
|
||||
Current: TeyrChain activation, collator configuration, HRMP channels.
|
||||
Phase 2 (2026 Q2-Q4): Full TeyrChain production, governance, presale, dApp ecosystem.
|
||||
Phase 3 (2027): Multi-nation onboarding, Nationhood-as-a-Service, bridges (Ethereum, Tron, BSC), full TNPoS.
|
||||
Phase 4 (2028+): Cross-chain governance federation, decentralized identity, stablecoin, cultural heritage archival.
|
||||
|
||||
License: Apache 2.0, Copyright 2026 Kurdistan Tech Institute. Lead Architect: SatoshiQaziMuhammed.
|
||||
|
||||
You are embedded as a chat assistant on the Pezkuwichain news site. Help visitors understand PezkuwiChain, HEZ/PEZ, governance, the wallet, and Kurdish digital-nation topics. If asked something you don't know, say so briefly. Never invent prices or figures.`;
|
||||
|
||||
const CORS = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
};
|
||||
const J = (o: unknown, s = 200) =>
|
||||
new Response(JSON.stringify(o), {
|
||||
status: s,
|
||||
headers: { ...CORS, 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
// Per-IP rate limit via a service-role table (cost guard for the public endpoint).
|
||||
async function allowed(ip: string): Promise<boolean> {
|
||||
if (!SUPABASE_URL || !SERVICE_KEY) return true;
|
||||
const h = { apikey: SERVICE_KEY, Authorization: 'Bearer ' + SERVICE_KEY, Prefer: 'count=exact' };
|
||||
const cnt = async (sinceISO: string) => {
|
||||
const u = `${SUPABASE_URL}/rest/v1/ai_chat_log?ip=eq.${encodeURIComponent(ip)}&created_at=gte.${sinceISO}&select=id`;
|
||||
const r = await fetch(u, { headers: { ...h, Range: '0-0' } });
|
||||
const cr = r.headers.get('content-range') || '*/0';
|
||||
return parseInt(cr.split('/')[1] || '0', 10);
|
||||
};
|
||||
const minAgo = new Date(Date.now() - 60_000).toISOString();
|
||||
const dayAgo = new Date(Date.now() - 86_400_000).toISOString();
|
||||
if ((await cnt(minAgo)) >= 8) return false; // 8 / minute
|
||||
if ((await cnt(dayAgo)) >= 120) return false; // 120 / day
|
||||
await fetch(`${SUPABASE_URL}/rest/v1/ai_chat_log`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: SERVICE_KEY,
|
||||
Authorization: 'Bearer ' + SERVICE_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ ip }),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') return new Response('ok', { headers: CORS });
|
||||
if (req.method !== 'POST') return J({ error: 'method' }, 405);
|
||||
try {
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const q = (body.q || body.question || '').toString().trim();
|
||||
if (!q || q.length > 2000) return J({ error: 'bad_request' }, 400);
|
||||
const ip = (req.headers.get('x-forwarded-for') || '').split(',')[0].trim() || 'unknown';
|
||||
if (!(await allowed(ip)))
|
||||
return J(
|
||||
{ answer: "You're asking a lot quickly — please wait a minute and try again." },
|
||||
429
|
||||
);
|
||||
const hist = Array.isArray(body.history)
|
||||
? body.history
|
||||
.filter(
|
||||
(m: any) =>
|
||||
m && (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string'
|
||||
)
|
||||
.slice(-6)
|
||||
: [];
|
||||
const messages = [...hist, { role: 'user', content: q }];
|
||||
const r = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': ANTHROPIC_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 900,
|
||||
system: SYSTEM,
|
||||
messages,
|
||||
}),
|
||||
});
|
||||
if (!r.ok) return J({ answer: 'Sorry, I could not answer right now. Please try again.' }, 200);
|
||||
const d = await r.json();
|
||||
const answer = (d.content && d.content[0] && d.content[0].text) || '…';
|
||||
return J({ answer }, 200);
|
||||
} catch (_e) {
|
||||
return J({ answer: 'Something went wrong. Please try again.' }, 200);
|
||||
}
|
||||
});
|
||||
@@ -26,6 +26,8 @@ const MINI_APP_URLS: Record<string, string> = {
|
||||
};
|
||||
|
||||
const ANTHROPIC_API_KEY = Deno.env.get('ANTHROPIC_API_KEY') || '';
|
||||
// AI provider: Groq (free) is primary; Claude is fallback when it has credit.
|
||||
const GROQ_API_KEY = Deno.env.get('GROQ_API_KEY') || '';
|
||||
|
||||
function getBotId(req: Request): string {
|
||||
const url = new URL(req.url);
|
||||
@@ -59,6 +61,9 @@ Cûzdanê xwe biafirînin, zimanê xwe hilbijêrin û welatiyê Pezkuwî bibin.
|
||||
|
||||
<i>Start your digital journey with Pezkuwi.
|
||||
Create your wallet, choose your language and become a citizen.</i>
|
||||
|
||||
🤖 Dijital Kurdistan AI agentıyla sohbet etmek ve daha detaylı bilgi almak için → @DKSkurdistanBot
|
||||
<i>Chat with Digital Kurdistan AI agent for more info → @DKSkurdistanBot</i>
|
||||
`;
|
||||
|
||||
// ── DKS bot (@DKSKurdistanBot) welcome ──────────────────────────────
|
||||
@@ -198,7 +203,7 @@ LINKS:
|
||||
Website: pezkuwichain.io
|
||||
GitHub: github.com/pezkuwichain/pezkuwi-sdk
|
||||
Discord: discord.gg/Y3VyEC6h8W
|
||||
Telegram Channel: t.me/dijitalkurdistan
|
||||
Telegram Channel: t.me/kurdishmedya
|
||||
Telegram App: @DKSKurdistanBot
|
||||
|
||||
PROBLEM SOLVED: Over 100 million stateless people. Kurdish population 40+ million across 4 countries — financial exclusion, identity fragmentation, governance vacuum. PezkuwiChain provides digital nation-state infrastructure.
|
||||
@@ -231,24 +236,59 @@ async function handleAIChat(token: string, chatId: number, userMessage: string,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': ANTHROPIC_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 1024,
|
||||
system: CLAUDE_SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
}),
|
||||
});
|
||||
// Primary provider: Groq (free). Fallback: Claude when it has credit.
|
||||
let aiReply: string | null = null;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('[AI] Claude API error:', response.status, errorText);
|
||||
if (GROQ_API_KEY) {
|
||||
try {
|
||||
const gr = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: 'Bearer ' + GROQ_API_KEY, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'llama-3.3-70b-versatile',
|
||||
temperature: 0.3,
|
||||
max_tokens: 900,
|
||||
messages: [
|
||||
{ role: 'system', content: CLAUDE_SYSTEM_PROMPT },
|
||||
{ role: 'user', content: userMessage },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (gr.ok) {
|
||||
const gd = await gr.json();
|
||||
aiReply = gd.choices?.[0]?.message?.content || null;
|
||||
} else {
|
||||
console.error('[AI] Groq error:', gr.status, await gr.text());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[AI] Groq exception:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!aiReply && ANTHROPIC_API_KEY) {
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': ANTHROPIC_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 1024,
|
||||
system: CLAUDE_SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
}),
|
||||
});
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
aiReply = result.content?.[0]?.text || null;
|
||||
} else {
|
||||
console.error('[AI] Claude API error:', response.status, await response.text());
|
||||
}
|
||||
}
|
||||
|
||||
if (!aiReply) {
|
||||
await sendTelegramRequest(token, 'sendMessage', {
|
||||
chat_id: chatId,
|
||||
text: 'Sorry, I could not process your question right now. Please try again.',
|
||||
@@ -256,9 +296,6 @@ async function handleAIChat(token: string, chatId: number, userMessage: string,
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const aiReply = result.content?.[0]?.text || 'No response generated.';
|
||||
|
||||
// Telegram message limit is 4096 chars
|
||||
if (aiReply.length <= 4096) {
|
||||
await sendTelegramRequest(token, 'sendMessage', {
|
||||
@@ -440,7 +477,7 @@ async function sendDksWelcome(token: string, chatId: number) {
|
||||
[
|
||||
{
|
||||
text: '📢 Join Channel / Kanalê Tev Bibin',
|
||||
url: 'https://t.me/dijitalkurdistan',
|
||||
url: 'https://t.me/kurdishmedya',
|
||||
},
|
||||
],
|
||||
],
|
||||
@@ -545,7 +582,7 @@ Just type your question in any language and I'll answer!
|
||||
|
||||
<b>Links:</b>
|
||||
🌐 Website: pezkuwichain.io
|
||||
📢 Channel: t.me/dijitalkurdistan
|
||||
📢 Channel: t.me/kurdishmedya
|
||||
💬 Discord: discord.gg/Y3VyEC6h8W
|
||||
`;
|
||||
await sendTelegramRequest(token, 'sendMessage', {
|
||||
|
||||
Reference in New Issue
Block a user