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