feat: update Be Citizen to new applyForCitizenship API

- Single tx (applyForCitizenship) instead of 2-step setIdentity+applyForKyc
- Keccak-256 identity hash via js-sha3
- Referral code replaced with referrer SS58 address
- Success screen shows pending referral status instead of citizen ID
- Updated all 6 translation files with new keys
This commit is contained in:
2026-02-14 22:00:32 +03:00
parent 59d4f3e6a1
commit f864ed6804
15 changed files with 154 additions and 203 deletions
+9 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pezkuwi-telegram-miniapp", "name": "pezkuwi-telegram-miniapp",
"version": "1.0.169", "version": "1.0.193",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pezkuwi-telegram-miniapp", "name": "pezkuwi-telegram-miniapp",
"version": "1.0.169", "version": "1.0.193",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@pezkuwi/api": "^16.5.36", "@pezkuwi/api": "^16.5.36",
@@ -20,6 +20,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"js-sha3": "^0.9.3",
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.3.1", "react": "^18.3.1",
@@ -7060,6 +7061,12 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/js-sha3": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz",
"integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pezkuwi-telegram-miniapp", "name": "pezkuwi-telegram-miniapp",
"version": "1.0.193", "version": "1.0.194",
"type": "module", "type": "module",
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
"author": "Pezkuwichain Team", "author": "Pezkuwichain Team",
@@ -48,6 +48,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"js-sha3": "^0.9.3",
"lucide-react": "^0.462.0", "lucide-react": "^0.462.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.3.1", "react": "^18.3.1",
+7 -7
View File
@@ -38,7 +38,7 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
const [region, setRegion] = useState<Region | ''>(''); const [region, setRegion] = useState<Region | ''>('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [profession, setProfession] = useState(''); const [profession, setProfession] = useState('');
const [referralCode, setReferralCode] = useState(''); const [referrerAddress, setReferrerAddress] = useState('');
const [consent, setConsent] = useState(false); const [consent, setConsent] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -120,7 +120,7 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
region: region as Region, region: region as Region,
email, email,
profession, profession,
referralCode: referralCode || undefined, referrerAddress: referrerAddress || undefined,
walletAddress, walletAddress,
timestamp: Date.now(), timestamp: Date.now(),
}; };
@@ -319,15 +319,15 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
/> />
</div> </div>
{/* Referral Code */} {/* Referrer Address */}
<div> <div>
<label className={labelClass}>{t('citizen.referralCode')}</label> <label className={labelClass}>{t('citizen.referrerAddress')}</label>
<input <input
type="text" type="text"
value={referralCode} value={referrerAddress}
onChange={(e) => setReferralCode(e.target.value)} onChange={(e) => setReferrerAddress(e.target.value)}
className={inputClass} className={inputClass}
placeholder={t('citizen.referralCodePlaceholder')} placeholder={t('citizen.referrerPlaceholder')}
/> />
</div> </div>
+20 -19
View File
@@ -11,16 +11,15 @@ import { useTelegram } from '@/hooks/useTelegram';
import { useWallet } from '@/contexts/WalletContext'; import { useWallet } from '@/contexts/WalletContext';
import type { CitizenshipData } from '@/lib/citizenship'; import type { CitizenshipData } from '@/lib/citizenship';
import { import {
generateCommitmentHash, calculateIdentityHash,
generateNullifierHash,
saveCitizenshipLocally, saveCitizenshipLocally,
uploadToIPFS, uploadToIPFS,
submitCitizenshipApplication, applyCitizenship,
} from '@/lib/citizenship'; } from '@/lib/citizenship';
interface Props { interface Props {
citizenshipData: CitizenshipData; citizenshipData: CitizenshipData;
onSuccess: (blockHash?: string) => void; onSuccess: (identityHash: string, blockHash?: string) => void;
onError: (error: string) => void; onError: (error: string) => void;
} }
@@ -32,23 +31,24 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
const { peopleApi, keypair } = useWallet(); const { peopleApi, keypair } = useWallet();
const [state, setState] = useState<ProcessingState>('preparing'); const [state, setState] = useState<ProcessingState>('preparing');
const [ipfsCid, setIpfsCid] = useState<string>(''); const [identityHash, setIdentityHash] = useState<string>('');
// Prepare data on mount // Prepare data on mount
useEffect(() => { useEffect(() => {
const prepare = async () => { const prepare = async () => {
try { try {
// Generate commitment hash // Mock IPFS upload
generateCommitmentHash(citizenshipData); const ipfsCid = await uploadToIPFS(citizenshipData);
generateNullifierHash(citizenshipData.walletAddress, citizenshipData.timestamp);
// Calculate identity hash (keccak256)
const hash = calculateIdentityHash(citizenshipData.fullName, citizenshipData.email, [
ipfsCid,
]);
setIdentityHash(hash);
// Save encrypted data locally // Save encrypted data locally
saveCitizenshipLocally(citizenshipData); saveCitizenshipLocally(citizenshipData);
// Mock IPFS upload
const cid = await uploadToIPFS(citizenshipData);
setIpfsCid(cid);
// Small delay to show animation // Small delay to show animation
await new Promise((resolve) => setTimeout(resolve, 1500)); await new Promise((resolve) => setTimeout(resolve, 1500));
@@ -72,18 +72,16 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
hapticImpact('medium'); hapticImpact('medium');
try { try {
const result = await submitCitizenshipApplication( const result = await applyCitizenship(
peopleApi, peopleApi,
keypair, keypair,
citizenshipData.fullName, identityHash,
citizenshipData.email, citizenshipData.referrerAddress || null
ipfsCid,
`Citizenship application - ${citizenshipData.region}`
); );
if (result.success) { if (result.success) {
hapticNotification('success'); hapticNotification('success');
onSuccess(result.blockHash); onSuccess(identityHash, result.blockHash);
} else { } else {
hapticNotification('error'); hapticNotification('error');
onError(result.error || t('citizen.submissionFailed')); onError(result.error || t('citizen.submissionFailed'));
@@ -96,7 +94,7 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
peopleApi, peopleApi,
keypair, keypair,
citizenshipData, citizenshipData,
ipfsCid, identityHash,
hapticImpact, hapticImpact,
hapticNotification, hapticNotification,
onSuccess, onSuccess,
@@ -124,6 +122,9 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
{state === 'preparing' && ( {state === 'preparing' && (
<p className="text-sm text-muted-foreground">{citizenshipData.fullName}</p> <p className="text-sm text-muted-foreground">{citizenshipData.fullName}</p>
)} )}
{state === 'ready' && (
<p className="text-xs text-muted-foreground">{t('citizen.depositRequired')}</p>
)}
</div> </div>
{/* Sign Button */} {/* Sign Button */}
+17 -12
View File
@@ -1,27 +1,24 @@
/** /**
* Citizen Success Screen * Citizen Success Screen
* Shows after successful citizenship application submission * Shows after successful citizenship application submission
* Displays pending referral status instead of final approval
*/ */
import { CheckCircle } from 'lucide-react'; import { CheckCircle, Clock } from 'lucide-react';
import { useTranslation } from '@/i18n'; import { useTranslation } from '@/i18n';
import { useTelegram } from '@/hooks/useTelegram'; import { useTelegram } from '@/hooks/useTelegram';
import { formatAddress } from '@/lib/wallet-service'; import { formatAddress } from '@/lib/wallet-service';
import { generateCitizenNumber } from '@/lib/citizenship';
interface Props { interface Props {
address: string; address: string;
identityHash: string;
onOpenApp: () => void; onOpenApp: () => void;
} }
export function CitizenSuccess({ address, onOpenApp }: Props) { export function CitizenSuccess({ address, identityHash, onOpenApp }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { hapticImpact } = useTelegram(); const { hapticImpact } = useTelegram();
// Generate a citizen number based on address
const citizenNumber = generateCitizenNumber(address, 42, 0);
const citizenId = `#42-0-${citizenNumber}`;
const handleOpenApp = () => { const handleOpenApp = () => {
hapticImpact('medium'); hapticImpact('medium');
onOpenApp(); onOpenApp();
@@ -36,15 +33,18 @@ export function CitizenSuccess({ address, onOpenApp }: Props) {
{/* Title */} {/* Title */}
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h1 className="text-2xl font-bold">{t('citizen.successTitle')}</h1> <h1 className="text-2xl font-bold">{t('citizen.applicationSubmitted')}</h1>
<p className="text-muted-foreground">{t('citizen.successSubtitle')}</p> <div className="flex items-center justify-center gap-2 text-yellow-500">
<Clock className="w-4 h-4" />
<p className="text-sm">{t('citizen.pendingReferral')}</p>
</div>
</div> </div>
{/* Citizen ID Card */} {/* Application Info Card */}
<div className="w-full max-w-sm bg-muted/50 rounded-2xl p-5 space-y-4 border border-border"> <div className="w-full max-w-sm bg-muted/50 rounded-2xl p-5 space-y-4 border border-border">
<div> <div>
<p className="text-xs text-muted-foreground">{t('citizen.citizenId')}</p> <p className="text-xs text-muted-foreground">{t('citizen.identityHash')}</p>
<p className="text-xl font-mono font-bold text-primary">{citizenId}</p> <p className="text-sm font-mono break-all">{formatAddress(identityHash)}</p>
</div> </div>
<div> <div>
<p className="text-xs text-muted-foreground">{t('citizen.walletAddress')}</p> <p className="text-xs text-muted-foreground">{t('citizen.walletAddress')}</p>
@@ -52,6 +52,11 @@ export function CitizenSuccess({ address, onOpenApp }: Props) {
</div> </div>
</div> </div>
{/* Info Note */}
<p className="text-xs text-muted-foreground text-center max-w-sm">
{t('citizen.applicationInfo')}
</p>
{/* Open App Button */} {/* Open App Button */}
<button <button
onClick={handleOpenApp} onClick={handleOpenApp}
+8 -5
View File
@@ -608,8 +608,8 @@ const ar: Translations = {
emailPlaceholder: 'name@mail.com', emailPlaceholder: 'name@mail.com',
profession: 'المهنة', profession: 'المهنة',
professionPlaceholder: 'أدخل مهنتك', professionPlaceholder: 'أدخل مهنتك',
referralCode: 'رمز الإحالة (اختياري)', referrerAddress: 'عنوان المُحيل (اختياري)',
referralCodePlaceholder: 'أدخل رمز الإحالة', referrerPlaceholder: 'أدخل عنوان SS58 - اتركه فارغاً للتعيين التلقائي',
consentCheckbox: 'بياناتي محمية بـ ZK-proof، فقط الهاش يُخزن على البلوكشين', consentCheckbox: 'بياناتي محمية بـ ZK-proof، فقط الهاش يُخزن على البلوكشين',
submit: 'إرسال', submit: 'إرسال',
sign: 'وقّع', sign: 'وقّع',
@@ -617,9 +617,11 @@ const ar: Translations = {
preparingData: 'جاري تحضير البيانات...', preparingData: 'جاري تحضير البيانات...',
readyToSign: 'جاهز للتوقيع', readyToSign: 'جاهز للتوقيع',
signingTx: 'جاري التوقيع...', signingTx: 'جاري التوقيع...',
successTitle: 'أصبحت مواطناً في بزكوي!', applicationSubmitted: 'تم استلام طلبك!',
successSubtitle: 'مرحباً بك في وطنك الرقمي!', pendingReferral: 'في انتظار موافقة المُحيل',
citizenId: 'رقم المواطنة', identityHash: 'هاش الهوية',
applicationInfo: 'بعد موافقة المُحيل، يمكنك التأكيد',
depositRequired: '١ HEZ وديعة مطلوبة',
walletAddress: 'عنوان المحفظة', walletAddress: 'عنوان المحفظة',
fillAllFields: 'يرجى ملء جميع الحقول', fillAllFields: 'يرجى ملء جميع الحقول',
acceptConsent: 'يرجى تحديد مربع الموافقة', acceptConsent: 'يرجى تحديد مربع الموافقة',
@@ -628,6 +630,7 @@ const ar: Translations = {
submissionFailed: 'فشل الإرسال', submissionFailed: 'فشل الإرسال',
alreadyPending: 'لديك طلب قيد الانتظار', alreadyPending: 'لديك طلب قيد الانتظار',
alreadyApproved: 'مواطنتك معتمدة بالفعل!', alreadyApproved: 'مواطنتك معتمدة بالفعل!',
insufficientBalance: 'رصيد غير كافٍ (١ HEZ وديعة مطلوبة)',
selectLanguage: 'اختر اللغة', selectLanguage: 'اختر اللغة',
}, },
}; };
+8 -5
View File
@@ -610,8 +610,8 @@ const ckb: Translations = {
emailPlaceholder: 'ناو@mail.com', emailPlaceholder: 'ناو@mail.com',
profession: 'پیشە', profession: 'پیشە',
professionPlaceholder: 'پیشەکەت بنووسە', professionPlaceholder: 'پیشەکەت بنووسە',
referralCode: 'کۆدی ڕیفێڕاڵ (ئارەزوومەندانە)', referrerAddress: 'ناونیشانی ڕیفێڕەر (ئارەزوومەندانە)',
referralCodePlaceholder: 'کۆدی ڕیفێڕاڵ بنووسە', referrerPlaceholder: 'ناونیشانی SS58 بنووسە - بەتاڵ بهێڵە بۆ دیاریکردنی ئۆتۆماتیک',
consentCheckbox: 'داتاکانم بە ZK-proof پارێزراون، تەنها هاش لە بلۆکچەین تۆمار دەکرێت', consentCheckbox: 'داتاکانم بە ZK-proof پارێزراون، تەنها هاش لە بلۆکچەین تۆمار دەکرێت',
submit: 'ناردن', submit: 'ناردن',
sign: 'واژووبکە', sign: 'واژووبکە',
@@ -619,9 +619,11 @@ const ckb: Translations = {
preparingData: 'داتا ئامادە دەکرێت...', preparingData: 'داتا ئامادە دەکرێت...',
readyToSign: 'ئامادەیە بۆ واژوو', readyToSign: 'ئامادەیە بۆ واژوو',
signingTx: 'واژوو دەکرێت...', signingTx: 'واژوو دەکرێت...',
successTitle: 'بوویت بە هاوڵاتی پێزکووی!', applicationSubmitted: 'داواکارییەکەت وەرگیرا!',
successSubtitle: 'بەخێر بێیت بۆ نیشتمانی دیجیتاڵت!', pendingReferral: 'چاوەڕوانی پەسەندکردنی ڕیفێڕەر',
citizenId: 'ناسنامەی هاوڵاتی', identityHash: 'هاشی ناسنامە',
applicationInfo: 'کاتێک ڕیفێڕەر پەسەند بکات، دەتوانیت confirm بکەیت',
depositRequired: '١ HEZ ئەمانەت پێویستە',
walletAddress: 'ناونیشانی جزدان', walletAddress: 'ناونیشانی جزدان',
fillAllFields: 'تکایە هەموو خانەکان پڕ بکەرەوە', fillAllFields: 'تکایە هەموو خانەکان پڕ بکەرەوە',
acceptConsent: 'تکایە خانەی ڕەزامەندی نیشان بدە', acceptConsent: 'تکایە خانەی ڕەزامەندی نیشان بدە',
@@ -630,6 +632,7 @@ const ckb: Translations = {
submissionFailed: 'ناردن سەرنەکەوت', submissionFailed: 'ناردن سەرنەکەوت',
alreadyPending: 'داواکارییەکی چاوەڕوانت هەیە', alreadyPending: 'داواکارییەکی چاوەڕوانت هەیە',
alreadyApproved: 'هاوڵاتیبوونت پێشتر پەسەند کراوە!', alreadyApproved: 'هاوڵاتیبوونت پێشتر پەسەند کراوە!',
insufficientBalance: 'باڵانسی پێویست نییە (١ HEZ ئەمانەت پێویستە)',
selectLanguage: 'زمان هەڵبژێرە', selectLanguage: 'زمان هەڵبژێرە',
}, },
}; };
+8 -5
View File
@@ -609,8 +609,8 @@ const en: Translations = {
emailPlaceholder: 'name@mail.com', emailPlaceholder: 'name@mail.com',
profession: 'Profession', profession: 'Profession',
professionPlaceholder: 'Enter your profession', professionPlaceholder: 'Enter your profession',
referralCode: 'Referral Code (Optional)', referrerAddress: 'Referrer Address (Optional)',
referralCodePlaceholder: 'Enter referral code', referrerPlaceholder: 'Enter SS58 address - leave empty for auto-assign',
consentCheckbox: 'My data is protected with ZK-proof, only a hash is stored on the blockchain', consentCheckbox: 'My data is protected with ZK-proof, only a hash is stored on the blockchain',
submit: 'Submit', submit: 'Submit',
sign: 'Sign', sign: 'Sign',
@@ -618,9 +618,11 @@ const en: Translations = {
preparingData: 'Preparing data...', preparingData: 'Preparing data...',
readyToSign: 'Ready to sign', readyToSign: 'Ready to sign',
signingTx: 'Signing...', signingTx: 'Signing...',
successTitle: 'You are now a Pezkuwi Citizen!', applicationSubmitted: 'Your application has been submitted!',
successSubtitle: 'Welcome to your digital homeland!', pendingReferral: 'Waiting for referrer approval',
citizenId: 'Citizen ID', identityHash: 'Identity Hash',
applicationInfo: 'Once your referrer approves, you can confirm',
depositRequired: '1 HEZ deposit required',
walletAddress: 'Wallet Address', walletAddress: 'Wallet Address',
fillAllFields: 'Please fill in all fields', fillAllFields: 'Please fill in all fields',
acceptConsent: 'Please accept the consent checkbox', acceptConsent: 'Please accept the consent checkbox',
@@ -629,6 +631,7 @@ const en: Translations = {
submissionFailed: 'Submission failed', submissionFailed: 'Submission failed',
alreadyPending: 'You already have a pending application', alreadyPending: 'You already have a pending application',
alreadyApproved: 'Your citizenship is already approved!', alreadyApproved: 'Your citizenship is already approved!',
insufficientBalance: 'Insufficient balance (1 HEZ deposit required)',
selectLanguage: 'Select language', selectLanguage: 'Select language',
}, },
}; };
+8 -5
View File
@@ -609,8 +609,8 @@ const fa: Translations = {
emailPlaceholder: 'name@mail.com', emailPlaceholder: 'name@mail.com',
profession: 'شغل', profession: 'شغل',
professionPlaceholder: 'شغل خود را وارد کنید', professionPlaceholder: 'شغل خود را وارد کنید',
referralCode: 'کد معرف (اختیاری)', referrerAddress: 'آدرس معرف (اختیاری)',
referralCodePlaceholder: 'کد معرف را وارد کنید', referrerPlaceholder: 'آدرس SS58 وارد کنید - خالی بگذارید برای تخصیص خودکار',
consentCheckbox: 'داده‌های من با ZK-proof محافظت می‌شوند، فقط هش در بلاکچین ثبت می‌شود', consentCheckbox: 'داده‌های من با ZK-proof محافظت می‌شوند، فقط هش در بلاکچین ثبت می‌شود',
submit: 'ارسال', submit: 'ارسال',
sign: 'امضا کنید', sign: 'امضا کنید',
@@ -618,9 +618,11 @@ const fa: Translations = {
preparingData: 'آماده‌سازی داده‌ها...', preparingData: 'آماده‌سازی داده‌ها...',
readyToSign: 'آماده برای امضا', readyToSign: 'آماده برای امضا',
signingTx: 'در حال امضا...', signingTx: 'در حال امضا...',
successTitle: 'شما شهروند پزکوی شدید!', applicationSubmitted: 'درخواست شما ثبت شد!',
successSubtitle: 'به سرزمین دیجیتالتان خوش آمدید!', pendingReferral: 'در انتظار تأیید معرف',
citizenId: 'شناسه شهروندی', identityHash: 'هش هویت',
applicationInfo: 'پس از تأیید معرف، می‌توانید تأیید نهایی کنید',
depositRequired: '۱ HEZ سپرده لازم است',
walletAddress: 'آدرس کیف پول', walletAddress: 'آدرس کیف پول',
fillAllFields: 'لطفاً همه فیلدها را پر کنید', fillAllFields: 'لطفاً همه فیلدها را پر کنید',
acceptConsent: 'لطفاً کادر رضایت را علامت بزنید', acceptConsent: 'لطفاً کادر رضایت را علامت بزنید',
@@ -629,6 +631,7 @@ const fa: Translations = {
submissionFailed: 'ارسال ناموفق', submissionFailed: 'ارسال ناموفق',
alreadyPending: 'درخواست در انتظار دارید', alreadyPending: 'درخواست در انتظار دارید',
alreadyApproved: 'شهروندی شما قبلاً تأیید شده!', alreadyApproved: 'شهروندی شما قبلاً تأیید شده!',
insufficientBalance: 'موجودی ناکافی (۱ HEZ سپرده لازم است)',
selectLanguage: 'انتخاب زبان', selectLanguage: 'انتخاب زبان',
}, },
}; };
+8 -5
View File
@@ -634,8 +634,8 @@ const krd: Translations = {
emailPlaceholder: 'navê@mail.com', emailPlaceholder: 'navê@mail.com',
profession: 'Pîşeya Te', profession: 'Pîşeya Te',
professionPlaceholder: 'Pîşeya xwe binivîse', professionPlaceholder: 'Pîşeya xwe binivîse',
referralCode: 'Koda Referral (Opsiyonel)', referrerAddress: 'Navnîşana Referrer (Opsiyonel)',
referralCodePlaceholder: 'Koda referral binivîse', referrerPlaceholder: 'Navnîşana SS58 binivîse - vala bimîne otomatîk tê danîn',
consentCheckbox: 'Daneyên min bi ZK-proof ewle ne, tenê hash li blockchain tê tomarkirin', consentCheckbox: 'Daneyên min bi ZK-proof ewle ne, tenê hash li blockchain tê tomarkirin',
submit: 'Bişîne', submit: 'Bişîne',
sign: 'Îmze Bike', sign: 'Îmze Bike',
@@ -643,9 +643,11 @@ const krd: Translations = {
preparingData: 'Dane têne amadekirin...', preparingData: 'Dane têne amadekirin...',
readyToSign: 'Ji bo îmzekirinê amade ye', readyToSign: 'Ji bo îmzekirinê amade ye',
signingTx: 'Tê îmzekirin...', signingTx: 'Tê îmzekirin...',
successTitle: 'Welatiyê Pezkuwî bûn!', applicationSubmitted: 'Serlêdana te hat qebûlkirin!',
successSubtitle: 'Serî hatî welatê xwe yê dîjîtal!', pendingReferral: 'Li benda pejirandina referrer',
citizenId: 'Nasnameya Welatî', identityHash: 'Hash-a Nasnameyê',
applicationInfo: 'Dema referrer pejirîne, hûn dikarin confirm bikin',
depositRequired: '1 HEZ depozîto pêwîst e',
walletAddress: 'Navnîşana Cûzdan', walletAddress: 'Navnîşana Cûzdan',
fillAllFields: 'Ji kerema xwe hemû qadan tije bike', fillAllFields: 'Ji kerema xwe hemû qadan tije bike',
acceptConsent: 'Ji kerema xwe qutiya pejirandinê nîşan bide', acceptConsent: 'Ji kerema xwe qutiya pejirandinê nîşan bide',
@@ -654,6 +656,7 @@ const krd: Translations = {
submissionFailed: 'Serlêdan neserketî', submissionFailed: 'Serlêdan neserketî',
alreadyPending: 'Serlêdanek te ya li bendê heye', alreadyPending: 'Serlêdanek te ya li bendê heye',
alreadyApproved: 'Welatîbûna te berê hatiye pejirandin!', alreadyApproved: 'Welatîbûna te berê hatiye pejirandin!',
insufficientBalance: 'Balansa têr nîne (1 HEZ depozîto pêwîst e)',
selectLanguage: 'Ziman hilbijêre', selectLanguage: 'Ziman hilbijêre',
}, },
}; };
+8 -5
View File
@@ -610,8 +610,8 @@ const tr: Translations = {
emailPlaceholder: 'isim@mail.com', emailPlaceholder: 'isim@mail.com',
profession: 'Meslek', profession: 'Meslek',
professionPlaceholder: 'Mesleğinizi girin', professionPlaceholder: 'Mesleğinizi girin',
referralCode: 'Referans Kodu (Opsiyonel)', referrerAddress: 'Referrer Adresi (Opsiyonel)',
referralCodePlaceholder: 'Referans kodunu girin', referrerPlaceholder: 'SS58 adresi girin - boş bırakılırsa otomatik atanır',
consentCheckbox: "Verilerim ZK-proof ile güvende, sadece hash blockchain'e kaydedilir", consentCheckbox: "Verilerim ZK-proof ile güvende, sadece hash blockchain'e kaydedilir",
submit: 'Gönder', submit: 'Gönder',
sign: 'İmzala', sign: 'İmzala',
@@ -619,9 +619,11 @@ const tr: Translations = {
preparingData: 'Veriler hazırlanıyor...', preparingData: 'Veriler hazırlanıyor...',
readyToSign: 'İmzalanmaya hazır', readyToSign: 'İmzalanmaya hazır',
signingTx: 'İmzalanıyor...', signingTx: 'İmzalanıyor...',
successTitle: 'Pezkuwi Vatandaşı Oldunuz!', applicationSubmitted: 'Başvurunuz alındı!',
successSubtitle: 'Dijital vatanınıza hoş geldiniz!', pendingReferral: 'Referrer onayı bekleniyor',
citizenId: 'Vatandaş No', identityHash: 'Kimlik Hash',
applicationInfo: 'Referrer onayladıktan sonra confirm edebilirsiniz',
depositRequired: '1 HEZ depozito gerekli',
walletAddress: 'Cüzdan Adresi', walletAddress: 'Cüzdan Adresi',
fillAllFields: 'Lütfen tüm alanları doldurun', fillAllFields: 'Lütfen tüm alanları doldurun',
acceptConsent: 'Lütfen onay kutusunu işaretleyin', acceptConsent: 'Lütfen onay kutusunu işaretleyin',
@@ -630,6 +632,7 @@ const tr: Translations = {
submissionFailed: 'Başvuru başarısız', submissionFailed: 'Başvuru başarısız',
alreadyPending: 'Bekleyen bir başvurunuz var', alreadyPending: 'Bekleyen bir başvurunuz var',
alreadyApproved: 'Vatandaşlığınız zaten onaylanmış!', alreadyApproved: 'Vatandaşlığınız zaten onaylanmış!',
insufficientBalance: 'Yetersiz bakiye (1 HEZ depozito gerekli)',
selectLanguage: 'Dil seçin', selectLanguage: 'Dil seçin',
}, },
}; };
+9 -6
View File
@@ -616,8 +616,8 @@ export interface Translations {
emailPlaceholder: string; emailPlaceholder: string;
profession: string; profession: string;
professionPlaceholder: string; professionPlaceholder: string;
referralCode: string; referrerAddress: string;
referralCodePlaceholder: string; referrerPlaceholder: string;
// Consent // Consent
consentCheckbox: string; consentCheckbox: string;
// Buttons // Buttons
@@ -628,11 +628,13 @@ export interface Translations {
preparingData: string; preparingData: string;
readyToSign: string; readyToSign: string;
signingTx: string; signingTx: string;
// Success // Application submitted
successTitle: string; applicationSubmitted: string;
successSubtitle: string; pendingReferral: string;
citizenId: string; identityHash: string;
walletAddress: string; walletAddress: string;
applicationInfo: string;
depositRequired: string;
// Errors // Errors
fillAllFields: string; fillAllFields: string;
acceptConsent: string; acceptConsent: string;
@@ -641,6 +643,7 @@ export interface Translations {
submissionFailed: string; submissionFailed: string;
alreadyPending: string; alreadyPending: string;
alreadyApproved: string; alreadyApproved: string;
insufficientBalance: string;
// Language selector // Language selector
selectLanguage: string; selectLanguage: string;
}; };
+31 -121
View File
@@ -4,12 +4,13 @@
* Uses native KeyringPair signing (no browser extension) * Uses native KeyringPair signing (no browser extension)
*/ */
import { keccak256 } from 'js-sha3';
import type { ApiPromise } from '@pezkuwi/api'; import type { ApiPromise } from '@pezkuwi/api';
import type { KeyringPair } from '@pezkuwi/keyring/types'; import type { KeyringPair } from '@pezkuwi/keyring/types';
// ── Type Definitions ──────────────────────────────────────────────── // ── Type Definitions ────────────────────────────────────────────────
export type KycStatus = 'NotStarted' | 'Pending' | 'Approved' | 'Rejected'; export type CitizenshipStatus = 'NotStarted' | 'PendingReferral' | 'ReferrerApproved' | 'Approved';
export type Region = 'bakur' | 'basur' | 'rojava' | 'rojhelat' | 'diaspora' | 'kurdistan_a_sor'; export type Region = 'bakur' | 'basur' | 'rojava' | 'rojhelat' | 'diaspora' | 'kurdistan_a_sor';
@@ -32,7 +33,7 @@ export interface CitizenshipData {
region: Region; region: Region;
email: string; email: string;
profession: string; profession: string;
referralCode?: string; referrerAddress?: string;
walletAddress: string; walletAddress: string;
timestamp: number; timestamp: number;
} }
@@ -41,28 +42,18 @@ export interface CitizenshipResult {
success: boolean; success: boolean;
error?: string; error?: string;
blockHash?: string; blockHash?: string;
identityHash?: string;
} }
// ── Hash Generation ───────────────────────────────────────────────── // ── Identity Hash (Keccak-256) ──────────────────────────────────────
function simpleHash(str: string): number { export function calculateIdentityHash(name: string, email: string, documentCids: string[]): string {
let hash = 0; const data = JSON.stringify({
for (let i = 0; i < str.length; i++) { name: name.trim().toLowerCase(),
const char = str.charCodeAt(i); email: email.trim().toLowerCase(),
hash = (hash << 5) - hash + char; documents: documentCids.sort(),
hash = hash & hash; });
} return '0x' + keccak256(data);
return hash;
}
export function generateCommitmentHash(data: CitizenshipData): string {
const str = JSON.stringify(data);
return simpleHash(str).toString(16);
}
export function generateNullifierHash(address: string, timestamp: number): string {
const str = address + timestamp.toString();
return 'nullifier_' + simpleHash(str).toString(16);
} }
// ── Encryption & Storage ──────────────────────────────────────────── // ── Encryption & Storage ────────────────────────────────────────────
@@ -99,9 +90,12 @@ export async function uploadToIPFS(_data: CitizenshipData): Promise<string> {
return mockCID; return mockCID;
} }
// ── KYC Status ────────────────────────────────────────────────────── // ── Citizenship Status ──────────────────────────────────────────────
export async function getKycStatus(api: ApiPromise, address: string): Promise<KycStatus> { export async function getCitizenshipStatus(
api: ApiPromise,
address: string
): Promise<CitizenshipStatus> {
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(api?.query as any)?.identityKyc) { if (!(api?.query as any)?.identityKyc) {
@@ -117,101 +111,34 @@ export async function getKycStatus(api: ApiPromise, address: string): Promise<Ky
const statusStr = status.toString(); const statusStr = status.toString();
if (statusStr === 'Approved') return 'Approved'; if (statusStr === 'Approved') return 'Approved';
if (statusStr === 'Pending') return 'Pending'; if (statusStr === 'PendingReferral') return 'PendingReferral';
if (statusStr === 'Rejected') return 'Rejected'; if (statusStr === 'ReferrerApproved') return 'ReferrerApproved';
return 'NotStarted'; return 'NotStarted';
} catch (error) { } catch (error) {
console.error('[Citizenship] Error fetching KYC status:', error); console.error('[Citizenship] Error fetching status:', error);
return 'NotStarted'; return 'NotStarted';
} }
} }
// ── Blockchain Submission ─────────────────────────────────────────── // ── Blockchain Submission ───────────────────────────────────────────
export async function submitCitizenshipApplication( export async function applyCitizenship(
api: ApiPromise, api: ApiPromise,
keypair: KeyringPair, keypair: KeyringPair,
name: string, identityHash: string,
email: string, referrerAddress: string | null = null
ipfsCid: string,
notes: string = 'Citizenship application via Telegram MiniApp'
): Promise<CitizenshipResult> { ): Promise<CitizenshipResult> {
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const tx = api.tx as any; const tx = api.tx as any;
if (!tx?.identityKyc?.setIdentity || !tx?.identityKyc?.applyForKyc) { if (!tx?.identityKyc?.applyForCitizenship) {
return { success: false, error: 'Identity KYC pallet not available' }; return { success: false, error: 'Identity KYC pallet not available' };
} }
const address = keypair.address; const result = await new Promise<CitizenshipResult>((resolve) => {
// Check for pending application
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pendingApp = await (api.query as any).identityKyc.pendingKycApplications(address);
if (!pendingApp.isEmpty) {
return {
success: false,
error: 'You already have a pending citizenship application.',
};
}
// Check if already approved
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const kycStatus = await (api.query as any).identityKyc.kycStatuses(address);
if (kycStatus.toString() === 'Approved') {
return {
success: false,
error: 'Your citizenship is already approved!',
};
}
const cidString = String(ipfsCid);
if (!cidString || cidString === 'undefined') {
return { success: false, error: 'Invalid IPFS CID' };
}
// Step 1: Set identity
const identityResult = await new Promise<CitizenshipResult>((resolve) => {
tx.identityKyc tx.identityKyc
.setIdentity(name, email) .applyForCitizenship(identityHash, referrerAddress)
.signAndSend(
keypair,
{ nonce: -1 },
({
status,
dispatchError,
}: {
status: { isInBlock: boolean; isFinalized: boolean };
dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string };
}) => {
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
let errorMessage = 'Identity transaction 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;
}
resolve({ success: true });
}
}
)
.catch((error: Error) => resolve({ success: false, error: error.message }));
});
if (!identityResult.success) {
return identityResult;
}
// Step 2: Apply for KYC
const kycResult = await new Promise<CitizenshipResult>((resolve) => {
tx.identityKyc
.applyForKyc(cidString, notes)
.signAndSend( .signAndSend(
keypair, keypair,
{ nonce: -1 }, { nonce: -1 },
@@ -229,25 +156,25 @@ export async function submitCitizenshipApplication(
}) => { }) => {
if (status.isInBlock || status.isFinalized) { if (status.isInBlock || status.isFinalized) {
if (dispatchError) { if (dispatchError) {
let errorMessage = 'KYC application failed'; let errorMessage = 'Citizenship application failed';
if (dispatchError.isModule) { if (dispatchError.isModule) {
const decoded = api.registry.findMetaError( const decoded = api.registry.findMetaError(
dispatchError.asModule as Parameters<typeof api.registry.findMetaError>[0] dispatchError.asModule as Parameters<typeof api.registry.findMetaError>[0]
); );
errorMessage = `${decoded.section}.${decoded.name}`; errorMessage = `${decoded.section}.${decoded.name}`;
} }
resolve({ success: false, error: errorMessage }); resolve({ success: false, error: errorMessage, identityHash });
return; return;
} }
const blockHash = status.asFinalized?.toString() || status.asInBlock?.toString(); const blockHash = status.asFinalized?.toString() || status.asInBlock?.toString();
resolve({ success: true, blockHash }); resolve({ success: true, blockHash, identityHash });
} }
} }
) )
.catch((error: Error) => resolve({ success: false, error: error.message })); .catch((error: Error) => resolve({ success: false, error: error.message, identityHash }));
}); });
return kycResult; return result;
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
@@ -255,20 +182,3 @@ export async function submitCitizenshipApplication(
}; };
} }
} }
// ── Citizen Number Generation ───────────────────────────────────────
export function generateCitizenNumber(
ownerAddress: string,
collectionId: number,
itemId: number
): string {
let hash = 0;
for (let i = 0; i < ownerAddress.length; i++) {
hash = (hash << 5) - hash + ownerAddress.charCodeAt(i);
hash = hash & hash;
}
hash += collectionId * 1000 + itemId;
hash = Math.abs(hash);
return (hash % 1000000).toString().padStart(6, '0');
}
+8 -2
View File
@@ -57,6 +57,7 @@ export function CitizenPage() {
const { t, lang, setLang } = useTranslation(); const { t, lang, setLang } = useTranslation();
const [showLangMenu, setShowLangMenu] = useState(false); const [showLangMenu, setShowLangMenu] = useState(false);
const [citizenshipData, setCitizenshipData] = useState<CitizenshipData | null>(null); const [citizenshipData, setCitizenshipData] = useState<CitizenshipData | null>(null);
const [identityHash, setIdentityHash] = useState<string>('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Determine initial step based on wallet state // Determine initial step based on wallet state
@@ -87,7 +88,8 @@ export function CitizenPage() {
}, []); }, []);
// Processing result // Processing result
const handleSuccess = useCallback(() => { const handleSuccess = useCallback((hash: string) => {
setIdentityHash(hash);
setStep('success'); setStep('success');
}, []); }, []);
@@ -186,7 +188,11 @@ export function CitizenPage() {
)} )}
{step === 'success' && address && ( {step === 'success' && address && (
<CitizenSuccess address={address} onOpenApp={handleOpenApp} /> <CitizenSuccess
address={address}
identityHash={identityHash}
onOpenApp={handleOpenApp}
/>
)} )}
</Suspense> </Suspense>
</main> </main>
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.0.193", "version": "1.0.194",
"buildTime": "2026-02-14T18:08:28.138Z", "buildTime": "2026-02-14T19:00:32.936Z",
"buildNumber": 1771092508139 "buildNumber": 1771095632937
} }