diff --git a/shared/lib/citizenship-workflow.ts b/shared/lib/citizenship-workflow.ts index 6d1eea12..df5d0a86 100644 --- a/shared/lib/citizenship-workflow.ts +++ b/shared/lib/citizenship-workflow.ts @@ -4,11 +4,13 @@ // Handles citizenship verification, status checks, and workflow logic import type { ApiPromise } from '@pezkuwi/api'; -import { web3Enable, web3FromAddress as web3FromAddressOriginal } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types'; import type { Signer } from '@pezkuwi/api/types'; +type WalletSource = 'extension' | 'walletconnect' | 'native' | null; + interface SignRawPayload { address: string; data: string; @@ -23,20 +25,6 @@ interface InjectedSigner { signRaw?: (payload: SignRawPayload) => Promise; } -interface InjectedExtension { - signer: Signer & InjectedSigner; -} - -// Use real extension in browser, throw error in unsupported environments -const web3FromAddress = async (address: string): Promise => { - // Check if we're in a browser environment with extension support - if (typeof window !== 'undefined') { - await web3Enable('PezkuwiChain'); - return web3FromAddressOriginal(address) as Promise; - } - throw new Error('Pezkuwi Wallet extension not available. Please install the extension.'); -}; - // ======================================== // TYPE DEFINITIONS // ======================================== @@ -477,7 +465,8 @@ export async function submitKycApplication( api: ApiPromise, account: InjectedAccountWithMeta, identityHash: string, - referrerAddress?: string + referrerAddress?: string, + walletSource?: WalletSource ): Promise<{ success: boolean; error?: string; blockHash?: string }> { try { if (!api?.tx?.identityKyc?.applyForCitizenship) { @@ -502,7 +491,7 @@ export async function submitKycApplication( }; } - const injector = await web3FromAddress(account.address); + const injector = await getSigner(account.address, walletSource ?? 'extension', api); if (import.meta.env.DEV) { console.log('=== submitKycApplication Debug ==='); @@ -629,14 +618,15 @@ export function subscribeToKycApproval( export async function approveReferral( api: ApiPromise, account: InjectedAccountWithMeta, - applicantAddress: string + applicantAddress: string, + walletSource?: WalletSource ): Promise<{ success: boolean; error?: string; blockHash?: string }> { try { if (!api?.tx?.identityKyc?.approveReferral) { return { success: false, error: 'Identity KYC pallet not available' }; } - const injector = await web3FromAddress(account.address); + const injector = await getSigner(account.address, walletSource ?? 'extension', api); const result = await new Promise<{ success: boolean; error?: string; blockHash?: string }>((resolve, reject) => { api.tx.identityKyc @@ -679,14 +669,15 @@ export async function approveReferral( */ export async function cancelApplication( api: ApiPromise, - account: InjectedAccountWithMeta + account: InjectedAccountWithMeta, + walletSource?: WalletSource ): Promise<{ success: boolean; error?: string }> { try { if (!api?.tx?.identityKyc?.cancelApplication) { return { success: false, error: 'Identity KYC pallet not available' }; } - const injector = await web3FromAddress(account.address); + const injector = await getSigner(account.address, walletSource ?? 'extension', api); const result = await new Promise<{ success: boolean; error?: string }>((resolve, reject) => { api.tx.identityKyc @@ -723,14 +714,15 @@ export async function cancelApplication( */ export async function confirmCitizenship( api: ApiPromise, - account: InjectedAccountWithMeta + account: InjectedAccountWithMeta, + walletSource?: WalletSource ): Promise<{ success: boolean; error?: string; blockHash?: string }> { try { if (!api?.tx?.identityKyc?.confirmCitizenship) { return { success: false, error: 'Identity KYC pallet not available' }; } - const injector = await web3FromAddress(account.address); + const injector = await getSigner(account.address, walletSource ?? 'extension', api); const result = await new Promise<{ success: boolean; error?: string; blockHash?: string }>((resolve, reject) => { api.tx.identityKyc @@ -850,10 +842,12 @@ export function generateAuthChallenge(tikiNumber: string): AuthChallenge { */ export async function signChallenge( account: InjectedAccountWithMeta, - challenge: AuthChallenge + challenge: AuthChallenge, + walletSource?: WalletSource, + api?: ApiPromise | null ): Promise { try { - const injector = await web3FromAddress(account.address); + const injector = await getSigner(account.address, walletSource ?? 'extension', api); if (!injector?.signer?.signRaw) { throw new Error('Signer not available'); diff --git a/web/src/components/AddLiquidityModal.tsx b/web/src/components/AddLiquidityModal.tsx index 9113cbc3..d6ecab67 100644 --- a/web/src/components/AddLiquidityModal.tsx +++ b/web/src/components/AddLiquidityModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { X, Plus, Info, AlertCircle } from 'lucide-react'; -import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { useWallet } from '@/contexts/WalletContext'; import { Button } from '@/components/ui/button'; @@ -62,7 +62,7 @@ export const AddLiquidityModal: React.FC = ({ asset1 = 1 // Default to PEZ }) => { // Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub) - const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi(); + const { assetHubApi, selectedAccount, isAssetHubReady, walletSource } = usePezkuwi(); const { balances, refreshBalances } = useWallet(); const [amount0, setAmount0] = useState(''); @@ -357,9 +357,8 @@ export const AddLiquidityModal: React.FC = ({ return; } - // Get the signer from the extension - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + // Get the signer (extension or WalletConnect) + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); // Convert amounts to proper decimals const amount0BN = BigInt(Math.floor(parseFloat(amount0) * Math.pow(10, asset0Decimals))); diff --git a/web/src/components/LPStakeModal.tsx b/web/src/components/LPStakeModal.tsx index a33c1900..04bb05b0 100644 --- a/web/src/components/LPStakeModal.tsx +++ b/web/src/components/LPStakeModal.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { X, Lock, AlertCircle, Loader2, Clock } from 'lucide-react'; -import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; @@ -50,7 +50,7 @@ export const LPStakeModal: React.FC = ({ onStakeSuccess, }) => { const { t } = useTranslation(); - const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi(); + const { assetHubApi, selectedAccount, isAssetHubReady, walletSource } = usePezkuwi(); const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); @@ -85,8 +85,7 @@ export const LPStakeModal: React.FC = ({ try { const amountBN = BigInt(Math.floor(amount * 1e12)); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); const tx = assetHubApi.tx.assetRewards.stake(poolId, amountBN.toString()); diff --git a/web/src/components/LPStakingModal.tsx b/web/src/components/LPStakingModal.tsx index 67dc9c0a..27bb820e 100644 --- a/web/src/components/LPStakingModal.tsx +++ b/web/src/components/LPStakingModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { X, Lock, Unlock, Gift, AlertCircle, Loader2, Info } from 'lucide-react'; -import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; @@ -31,7 +31,7 @@ const LP_TOKEN_NAMES: Record = { }; export const LPStakingModal: React.FC = ({ isOpen, onClose }) => { - const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi(); + const { assetHubApi, selectedAccount, isAssetHubReady, walletSource } = usePezkuwi(); const { t } = useTranslation(); const [pools, setPools] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -143,8 +143,7 @@ export const LPStakingModal: React.FC = ({ isOpen, onClose if (!pool) throw new Error('Pool not found'); const amountBN = BigInt(Math.floor(parseFloat(stakeAmount) * 1e12)); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); const tx = assetHubApi.tx.assetRewards.stake(selectedPool, amountBN.toString()); @@ -190,8 +189,7 @@ export const LPStakingModal: React.FC = ({ isOpen, onClose if (!pool) throw new Error('Pool not found'); const amountBN = BigInt(Math.floor(parseFloat(unstakeAmount) * 1e12)); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); const tx = assetHubApi.tx.assetRewards.unstake(selectedPool, amountBN.toString()); @@ -233,8 +231,7 @@ export const LPStakingModal: React.FC = ({ isOpen, onClose setSuccess(null); try { - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); const tx = assetHubApi.tx.assetRewards.harvestRewards(selectedPool); await new Promise((resolve, reject) => { diff --git a/web/src/components/RemoveLiquidityModal.tsx b/web/src/components/RemoveLiquidityModal.tsx index 6bac3c18..ebbef4b4 100644 --- a/web/src/components/RemoveLiquidityModal.tsx +++ b/web/src/components/RemoveLiquidityModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { X, Minus, AlertCircle, Info } from 'lucide-react'; -import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { useWallet } from '@/contexts/WalletContext'; import { Button } from '@/components/ui/button'; @@ -63,7 +63,7 @@ export const RemoveLiquidityModal: React.FC = ({ asset1, }) => { // Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub) - const { assetHubApi, selectedAccount } = usePezkuwi(); + const { assetHubApi, selectedAccount, walletSource } = usePezkuwi(); const { refreshBalances } = useWallet(); const [percentage, setPercentage] = useState(100); @@ -159,9 +159,8 @@ export const RemoveLiquidityModal: React.FC = ({ setError(null); try { - // Get the signer from the extension - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + // Get the signer (extension or WalletConnect) + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); // Get decimals for each asset const asset0Decimals = getAssetDecimals(asset0); diff --git a/web/src/components/TokenSwap.tsx b/web/src/components/TokenSwap.tsx index f7697859..18a62066 100644 --- a/web/src/components/TokenSwap.tsx +++ b/web/src/components/TokenSwap.tsx @@ -25,7 +25,7 @@ const AVAILABLE_TOKENS = [ const TokenSwap = () => { // Use Asset Hub API for DEX operations (assetConversion pallet is on Asset Hub) - const { assetHubApi, isAssetHubReady, selectedAccount } = usePezkuwi(); + const { assetHubApi, isAssetHubReady, selectedAccount, walletSource } = usePezkuwi(); const { balances, refreshBalances } = useWallet(); const { toast } = useToast(); const { t } = useTranslation(); @@ -583,10 +583,9 @@ const TokenSwap = () => { minAmountOut: minAmountOut.toString() }); - // Get signer from extension - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + // Get signer (extension or WalletConnect) + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); // Build transaction based on token types let tx; diff --git a/web/src/components/TransferModal.tsx b/web/src/components/TransferModal.tsx index 494133b9..fc91dae2 100644 --- a/web/src/components/TransferModal.tsx +++ b/web/src/components/TransferModal.tsx @@ -67,7 +67,7 @@ const TOKENS: Token[] = [ ]; export const TransferModal: React.FC = ({ isOpen, onClose, selectedAsset }) => { - const { api, assetHubApi, isApiReady, isAssetHubReady, selectedAccount } = usePezkuwi(); + const { api, assetHubApi, isApiReady, isAssetHubReady, selectedAccount, walletSource } = usePezkuwi(); const { toast } = useToast(); const { t } = useTranslation(); @@ -129,10 +129,9 @@ export const TransferModal: React.FC = ({ isOpen, onClose, s setTxStatus('signing'); try { - // Import web3FromAddress to get the injector - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + // Get signer (extension or WalletConnect) + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, isAssetHubTransfer ? assetHubApi : api); // Convert amount to smallest unit const amountInSmallestUnit = BigInt(parseFloat(amount) * Math.pow(10, currentToken.decimals)); diff --git a/web/src/components/USDTBridge.tsx b/web/src/components/USDTBridge.tsx index 242d9674..937af008 100644 --- a/web/src/components/USDTBridge.tsx +++ b/web/src/components/USDTBridge.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { X, ArrowDown, ArrowUp, AlertCircle, Info, Clock, CheckCircle2 } from 'lucide-react'; -import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { useWallet } from '@/contexts/WalletContext'; import { Button } from '@/components/ui/button'; @@ -30,7 +30,7 @@ export const USDTBridge: React.FC = ({ specificAddresses = {}, }) => { const { t } = useTranslation(); - const { api, selectedAccount, isApiReady } = usePezkuwi(); + const { api, selectedAccount, isApiReady, walletSource } = usePezkuwi(); const { refreshBalances } = useWallet(); const [depositAmount, setDepositAmount] = useState(''); @@ -114,8 +114,7 @@ export const USDTBridge: React.FC = ({ setSuccess(null); try { - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, api); // Burn wUSDT const amountBN = BigInt(Math.floor(amount * 1e6)); // 6 decimals diff --git a/web/src/components/XCMTeleportModal.tsx b/web/src/components/XCMTeleportModal.tsx index 0e530548..d3d9bc72 100644 --- a/web/src/components/XCMTeleportModal.tsx +++ b/web/src/components/XCMTeleportModal.tsx @@ -54,7 +54,7 @@ interface XCMTeleportModalProps { } export const XCMTeleportModal: React.FC = ({ isOpen, onClose }) => { - const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount } = usePezkuwi(); + const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount, walletSource } = usePezkuwi(); const { toast } = useToast(); const { t } = useTranslation(); @@ -157,9 +157,8 @@ export const XCMTeleportModal: React.FC = ({ isOpen, onCl setTxStatus('signing'); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); // Convert to smallest unit (12 decimals) const amountInSmallestUnit = BigInt(Math.floor(parseFloat(amount) * 1e12)); diff --git a/web/src/components/admin/CommissionSetupTab.tsx b/web/src/components/admin/CommissionSetupTab.tsx index dc4b2630..55932ec9 100644 --- a/web/src/components/admin/CommissionSetupTab.tsx +++ b/web/src/components/admin/CommissionSetupTab.tsx @@ -11,7 +11,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; export function CommissionSetupTab() { const { t } = useTranslation(); - const { api, isApiReady, selectedAccount } = usePezkuwi(); + const { api, isApiReady, selectedAccount, walletSource } = usePezkuwi(); const { toast } = useToast(); const [loading, setLoading] = useState(true); @@ -70,9 +70,8 @@ export function CommissionSetupTab() { setProcessing(true); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); // Parse addresses (one per line, trim whitespace) const newAddresses = newMemberAddress @@ -175,9 +174,8 @@ export function CommissionSetupTab() { setProcessing(true); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); if (import.meta.env.DEV) console.log('Initializing KYC Commission...'); if (import.meta.env.DEV) console.log('Proxy account:', COMMISSIONS.KYC.proxyAccount); diff --git a/web/src/components/admin/CommissionVotingTab.tsx b/web/src/components/admin/CommissionVotingTab.tsx index 51fedf87..4434a08e 100644 --- a/web/src/components/admin/CommissionVotingTab.tsx +++ b/web/src/components/admin/CommissionVotingTab.tsx @@ -28,7 +28,7 @@ interface Proposal { export function CommissionVotingTab() { const { t } = useTranslation(); - const { api, isApiReady, selectedAccount } = usePezkuwi(); + const { api, isApiReady, selectedAccount, walletSource } = usePezkuwi(); const { toast } = useToast(); const [loading, setLoading] = useState(true); @@ -150,9 +150,8 @@ export function CommissionVotingTab() { setVoting(proposal.hash); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); if (import.meta.env.DEV) console.log(`Voting ${approve ? 'AYE' : 'NAY'} on proposal:`, proposal.hash); @@ -258,9 +257,8 @@ export function CommissionVotingTab() { setVoting(proposal.hash); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); if (import.meta.env.DEV) console.log('Executing proposal:', proposal.hash); @@ -433,9 +431,8 @@ export function CommissionVotingTab() { if (!api || !selectedAccount) return; try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); // Get current members const currentMembers = await api.query.dynamicCommissionCollective.members(); diff --git a/web/src/components/admin/KycApprovalTab.tsx b/web/src/components/admin/KycApprovalTab.tsx index 6d47cf8e..433b80ad 100644 --- a/web/src/components/admin/KycApprovalTab.tsx +++ b/web/src/components/admin/KycApprovalTab.tsx @@ -21,7 +21,7 @@ import type { PendingApproval } from '@pezkuwi/lib/citizenship-workflow'; export function KycApprovalTab() { const { t } = useTranslation(); // identityKyc pallet is on People Chain - use peopleApi - const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi(); + const { peopleApi, isPeopleReady, selectedAccount, connectWallet, walletSource } = usePezkuwi(); const { toast } = useToast(); const [loading, setLoading] = useState(true); @@ -75,7 +75,7 @@ export function KycApprovalTab() { setProcessingAddress(applicantAddress); try { - const result = await approveReferral(peopleApi, selectedAccount, applicantAddress); + const result = await approveReferral(peopleApi, selectedAccount, applicantAddress, walletSource); if (!result.success) { toast({ diff --git a/web/src/components/citizenship/ExistingCitizenAuth.tsx b/web/src/components/citizenship/ExistingCitizenAuth.tsx index a12b7f8f..20c687af 100644 --- a/web/src/components/citizenship/ExistingCitizenAuth.tsx +++ b/web/src/components/citizenship/ExistingCitizenAuth.tsx @@ -17,7 +17,7 @@ interface ExistingCitizenAuthProps { export const ExistingCitizenAuth: React.FC = ({ onClose }) => { const { t } = useTranslation(); - const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi(); + const { peopleApi, isPeopleReady, selectedAccount, connectWallet, walletSource } = usePezkuwi(); const [citizenNumber, setCitizenNumber] = useState(''); const [step, setStep] = useState<'input' | 'verifying' | 'signing' | 'success' | 'error'>('input'); @@ -69,7 +69,7 @@ export const ExistingCitizenAuth: React.FC = ({ onClos try { // Sign the challenge - const signature = await signChallenge(selectedAccount, challenge); + const signature = await signChallenge(selectedAccount, challenge, walletSource, peopleApi); // Verify signature (self-verification for demonstration) const isValid = await verifySignature(signature, challenge, selectedAccount.address); diff --git a/web/src/components/citizenship/NewCitizenApplication.tsx b/web/src/components/citizenship/NewCitizenApplication.tsx index 81011253..da4490dc 100644 --- a/web/src/components/citizenship/NewCitizenApplication.tsx +++ b/web/src/components/citizenship/NewCitizenApplication.tsx @@ -25,7 +25,7 @@ type FormData = Omit; export const NewCitizenApplication: React.FC = ({ onClose, referrerAddress }) => { // identityKyc pallet is on People Chain - const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi(); + const { peopleApi, isPeopleReady, selectedAccount, connectWallet, walletSource } = usePezkuwi(); const { t } = useTranslation(); const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm(); @@ -51,7 +51,7 @@ export const NewCitizenApplication: React.FC = ({ on setConfirming(true); setError(null); try { - const result = await confirmCitizenship(peopleApi, selectedAccount); + const result = await confirmCitizenship(peopleApi, selectedAccount, walletSource); if (!result.success) { setError(result.error || t('newCitizen.failedToConfirm')); @@ -83,7 +83,7 @@ export const NewCitizenApplication: React.FC = ({ on setCanceling(true); setError(null); try { - const result = await cancelApplication(peopleApi, selectedAccount); + const result = await cancelApplication(peopleApi, selectedAccount, walletSource); if (!result.success) { setError(result.error || t('newCitizen.failedToCancel')); @@ -243,7 +243,8 @@ export const NewCitizenApplication: React.FC = ({ on peopleApi, selectedAccount, identityHash, - effectiveReferrer + effectiveReferrer, + walletSource ); if (!result.success) { diff --git a/web/src/components/dashboard/CommissionProposalsCard.tsx b/web/src/components/dashboard/CommissionProposalsCard.tsx index cf711b7b..590601a5 100644 --- a/web/src/components/dashboard/CommissionProposalsCard.tsx +++ b/web/src/components/dashboard/CommissionProposalsCard.tsx @@ -19,7 +19,7 @@ interface Proposal { export function CommissionProposalsCard() { const { t } = useTranslation(); - const { api, isApiReady, selectedAccount } = usePezkuwi(); + const { api, isApiReady, selectedAccount, walletSource } = usePezkuwi(); const { toast } = useToast(); const [loading, setLoading] = useState(true); @@ -109,9 +109,8 @@ export function CommissionProposalsCard() { setVoting(proposal.hash); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); const tx = api.tx.dynamicCommissionCollective.vote( proposal.hash, @@ -200,9 +199,8 @@ export function CommissionProposalsCard() { setVoting(proposal.hash); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, api); // Get proposal length bound const proposalOption = await api.query.dynamicCommissionCollective.proposalOf(proposal.hash); diff --git a/web/src/components/referral/InviteUserModal.tsx b/web/src/components/referral/InviteUserModal.tsx index 0c3645ab..e01dfd5c 100644 --- a/web/src/components/referral/InviteUserModal.tsx +++ b/web/src/components/referral/InviteUserModal.tsx @@ -23,7 +23,7 @@ interface InviteUserModalProps { export const InviteUserModal: React.FC = ({ isOpen, onClose }) => { const { t } = useTranslation(); - const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi(); + const { peopleApi, isPeopleReady, selectedAccount, walletSource } = usePezkuwi(); const [copied, setCopied] = useState(false); const [inviteeAddress, setInviteeAddress] = useState(''); const [initiating, setInitiating] = useState(false); @@ -81,9 +81,8 @@ export const InviteUserModal: React.FC = ({ isOpen, onClos setInitiateSuccess(false); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const { getSigner } = await import('@/lib/get-signer'); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); if (import.meta.env.DEV) console.log(`Initiating referral from ${selectedAccount.address} to ${inviteeAddress}...`); diff --git a/web/src/components/referral/ReferralDashboard.tsx b/web/src/components/referral/ReferralDashboard.tsx index 658ed52b..f5e5e233 100644 --- a/web/src/components/referral/ReferralDashboard.tsx +++ b/web/src/components/referral/ReferralDashboard.tsx @@ -14,7 +14,7 @@ import type { PendingApproval } from '@pezkuwi/lib/citizenship-workflow'; export const ReferralDashboard: React.FC = () => { const { t } = useTranslation(); const { stats, myReferrals, loading } = useReferral(); - const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi(); + const { peopleApi, isPeopleReady, selectedAccount, walletSource } = usePezkuwi(); const { toast } = useToast(); const [showInviteModal, setShowInviteModal] = useState(false); const [pendingApprovals, setPendingApprovals] = useState([]); @@ -45,7 +45,7 @@ export const ReferralDashboard: React.FC = () => { setProcessingAddress(applicantAddress); try { - const result = await approveReferral(peopleApi, selectedAccount, applicantAddress); + const result = await approveReferral(peopleApi, selectedAccount, applicantAddress, walletSource); if (result.success) { toast({ title: 'Referral Approved', diff --git a/web/src/components/staking/StakingDashboard.tsx b/web/src/components/staking/StakingDashboard.tsx index 468a6b1a..8231b786 100644 --- a/web/src/components/staking/StakingDashboard.tsx +++ b/web/src/components/staking/StakingDashboard.tsx @@ -11,7 +11,7 @@ import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { useWallet } from '@/contexts/WalletContext'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; -import { web3FromAddress, web3Enable } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { getStakingInfo, getActiveValidators, @@ -31,21 +31,8 @@ import { LoadingState } from '@pezkuwi/components/AsyncComponent'; import { ValidatorPoolDashboard } from './ValidatorPoolDashboard'; import { handleBlockchainError, handleBlockchainSuccess } from '@pezkuwi/lib/error-handler'; -// Get signer with auto-reconnect if extension session expired -async function getInjectorSigner(address: string) { - let injector = await web3FromAddress(address); - if (!injector?.signer) { - await web3Enable('PezkuwiChain'); - injector = await web3FromAddress(address); - if (!injector?.signer) { - throw new Error('Wallet signer not available. Please reconnect your wallet.'); - } - } - return injector; -} - export const StakingDashboard: React.FC = () => { - const { assetHubApi, peopleApi, selectedAccount, isAssetHubReady, isPeopleReady } = usePezkuwi(); + const { assetHubApi, peopleApi, selectedAccount, isAssetHubReady, isPeopleReady, walletSource } = usePezkuwi(); const { balances, refreshBalances } = useWallet(); const { t } = useTranslation(); @@ -129,7 +116,7 @@ export const StakingDashboard: React.FC = () => { setIsRecordingScore(true); try { - const injector = await getInjectorSigner(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); const result = await recordTrustScore(peopleApi, selectedAccount.address, injector.signer); if (result.success) { @@ -157,7 +144,7 @@ export const StakingDashboard: React.FC = () => { setIsClaimingReward(true); try { - const injector = await getInjectorSigner(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); const result = await claimPezReward(peopleApi, selectedAccount.address, epochIndex, injector.signer); if (result.success) { @@ -198,7 +185,7 @@ export const StakingDashboard: React.FC = () => { throw new Error(t('staking.insufficientHez')); } - const injector = await getInjectorSigner(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); // If already bonded, use bondExtra, otherwise use bond let tx; @@ -251,7 +238,7 @@ export const StakingDashboard: React.FC = () => { setIsLoading(true); try { - const injector = await getInjectorSigner(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); const tx = assetHubApi.tx.staking.nominate(selectedValidators); @@ -294,7 +281,7 @@ export const StakingDashboard: React.FC = () => { throw new Error(t('staking.insufficientStaked')); } - const injector = await getInjectorSigner(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); const tx = assetHubApi.tx.staking.unbond(amount); await tx.signAndSend( @@ -338,7 +325,7 @@ export const StakingDashboard: React.FC = () => { setIsLoading(true); try { - const injector = await getInjectorSigner(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, assetHubApi); // Number of slashing spans (usually 0) const tx = assetHubApi.tx.staking.withdrawUnbonded(0); @@ -381,7 +368,7 @@ export const StakingDashboard: React.FC = () => { setIsLoading(true); try { - const injector = await getInjectorSigner(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); // stakingScore pallet is on People Chain - uses cached staking data from Asset Hub const tx = peopleApi.tx.stakingScore.startScoreTracking(); diff --git a/web/src/lib/get-signer.ts b/web/src/lib/get-signer.ts new file mode 100644 index 00000000..63079710 --- /dev/null +++ b/web/src/lib/get-signer.ts @@ -0,0 +1,40 @@ +/** + * Universal signer helper - works with both browser extension and WalletConnect + * + * Usage: + * const injector = await getSigner(selectedAccount.address, walletSource, api); + * // injector.signer works for signAndSend, signRaw, etc. + */ + +import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { createWCSigner, isWCConnected, validateSession } from '@/lib/walletconnect-service'; +import type { ApiPromise } from '@pezkuwi/api'; + +type WalletSource = 'extension' | 'walletconnect' | 'native' | null; + +interface SignerResult { + signer: any; // Compatible with @pezkuwi/api Signer +} + +export async function getSigner( + address: string, + walletSource: WalletSource, + api?: ApiPromise | null +): Promise { + if (walletSource === 'walletconnect') { + if (!isWCConnected() || !validateSession()) { + throw new Error('WalletConnect session expired. Please reconnect your wallet.'); + } + if (!api) { + throw new Error('API not ready'); + } + const genesisHash = api.genesisHash.toHex(); + const wcSigner = createWCSigner(genesisHash, address); + return { signer: wcSigner }; + } + + // Extension or native: use web3FromAddress + await web3Enable('PezkuwiChain'); + const injector = await web3FromAddress(address); + return injector; +} diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 2a4c09f2..3b5f8e3c 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -12,7 +12,7 @@ import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowL import { useToast } from '@/hooks/use-toast'; import { fetchUserTikis, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories, getAllTikiNFTDetails, generateCitizenNumber, type TikiNFTDetails } from '@pezkuwi/lib/tiki'; import { getAllScores, getStakingScoreStatus, startScoreTracking, getPezRewards, recordTrustScore, claimPezReward, type UserScores, type StakingScoreStatus, type PezRewardInfo, formatDuration } from '@pezkuwi/lib/scores'; -import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { getKycStatus } from '@pezkuwi/lib/kyc'; import { ReferralDashboard } from '@/components/referral/ReferralDashboard'; // Commission proposals card removed - no longer using notary system for KYC approval @@ -21,7 +21,7 @@ import { ReferralDashboard } from '@/components/referral/ReferralDashboard'; export default function Dashboard() { const { t } = useTranslation(); const { user } = useAuth(); - const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount } = usePezkuwi(); + const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount, walletSource } = usePezkuwi(); const navigate = useNavigate(); const { toast } = useToast(); const [profile, setProfile] = useState | null>(null); @@ -157,8 +157,7 @@ export default function Dashboard() { setStartingScoreTracking(true); try { - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); // startScoreTracking on People Chain - staking data comes from Asset Hub via XCM const result = await startScoreTracking(peopleApi, selectedAccount.address, injector.signer); @@ -193,8 +192,7 @@ export default function Dashboard() { setIsRecordingScore(true); try { - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); const result = await recordTrustScore(peopleApi, selectedAccount.address, injector.signer); if (result.success) { @@ -215,8 +213,7 @@ export default function Dashboard() { setIsClaimingReward(true); try { - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); const result = await claimPezReward(peopleApi, selectedAccount.address, epochIndex, injector.signer); if (result.success) { @@ -332,9 +329,7 @@ export default function Dashboard() { setRenouncingCitizenship(true); try { - const { web3Enable, web3FromAddress } = await import('@pezkuwi/extension-dapp'); - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); if (import.meta.env.DEV) console.log('Renouncing citizenship...'); diff --git a/web/src/pages/WalletDashboard.tsx b/web/src/pages/WalletDashboard.tsx index 53891140..6188ba0f 100644 --- a/web/src/pages/WalletDashboard.tsx +++ b/web/src/pages/WalletDashboard.tsx @@ -10,7 +10,7 @@ import { NftList } from '@/components/NftList'; import { Button } from '@/components/ui/button'; import { ArrowUpRight, ArrowDownRight, History, ArrowLeft, RefreshCw, Coins, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; -import { web3Enable, web3FromAddress } from '@pezkuwi/extension-dapp'; +import { getSigner } from '@/lib/get-signer'; import { getPezRewards, recordTrustScore, claimPezReward, type PezRewardInfo } from '@pezkuwi/lib/scores'; interface Transaction { @@ -29,7 +29,7 @@ interface Transaction { const WalletDashboard: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount } = usePezkuwi(); + const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount, walletSource } = usePezkuwi(); const [isTransferModalOpen, setIsTransferModalOpen] = useState(false); const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false); const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); @@ -239,8 +239,7 @@ const WalletDashboard: React.FC = () => { if (!peopleApi || !selectedAccount) return; setIsRecordingScore(true); try { - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); const result = await recordTrustScore(peopleApi, selectedAccount.address, injector.signer); if (result.success) { toast.success(t('wallet.trustScoreRecorded')); @@ -260,8 +259,7 @@ const WalletDashboard: React.FC = () => { if (!peopleApi || !selectedAccount) return; setIsClaimingReward(true); try { - await web3Enable('PezkuwiChain'); - const injector = await web3FromAddress(selectedAccount.address); + const injector = await getSigner(selectedAccount.address, walletSource, peopleApi); const result = await claimPezReward(peopleApi, selectedAccount.address, epochIndex, injector.signer); if (result.success) { const rewardInfo = pezRewards?.claimableRewards.find(r => r.epoch === epochIndex);