Add multisig pending-operations UI, fix broken multisig address calc, new signing portal

- calculateMultisigAddress was completely broken (hex-decoded an SS58 string
  and never hashed the preimage) - fixed via @pezkuwi/util-crypto's real
  encodeMultiAddress/createKeyMulti, verified against the actual known
  multisig address on-chain.
- ReservesDashboardPage had stale Noter/Berdevk addresses that don't match
  the real signers - centralized as BRIDGE_MULTISIG_SPECIFIC_ADDRESSES.
- USDTBridge withdrawal called assets.burn directly as a single-signer
  extrinsic (always fails - only the multisig is Admin) while only
  checking status.isFinalized (a failed dispatch is still finalized, so it
  silently did nothing) - replaced with the correct transfer-to-custody
  flow the relayer actually watches for.
- New MultisigOperationsPage (/multisig/pending) lists pending calls from
  real Multisig.Multisigs storage and lets any of the 5 signers
  approve/reject with their own wallet extension.
- New standalone sign/ app (deployed separately at
  pezbridge-sign.pex.mom) - a dedicated, gated signing portal for the same
  operations, so signing isn't dependent on this app alone.
This commit is contained in:
2026-07-14 07:19:34 -07:00
parent fe3c65a706
commit dd58fe9164
24 changed files with 5662 additions and 64 deletions
+6
View File
@@ -32,6 +32,7 @@ const ProfileSettings = lazy(() => import('@/pages/ProfileSettings'));
const AdminPanel = lazy(() => import('@/pages/AdminPanel'));
const WalletDashboard = lazy(() => import('./pages/WalletDashboard'));
const ReservesDashboardPage = lazy(() => import('./pages/ReservesDashboardPage'));
const MultisigOperationsPage = lazy(() => import('./pages/MultisigOperationsPage'));
const BeCitizen = lazy(() => import('./pages/BeCitizen'));
const Identity = lazy(() => import('./pages/Identity'));
const Bereketli = lazy(() => import('./pages/Bereketli'));
@@ -192,6 +193,11 @@ function App() {
<ReservesDashboardPage />
</ProtectedRoute>
} />
<Route path="/multisig/pending" element={
<ProtectedRoute requireMultisigMember>
<MultisigOperationsPage />
</ProtectedRoute>
} />
<Route path="/elections" element={
<ProtectedRoute>
<Elections />
+301
View File
@@ -0,0 +1,301 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Shield, RefreshCw, CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { getSigner } from '@/lib/get-signer';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import {
getMultisigMembers,
calculateMultisigAddress,
approveMultisigTx,
cancelMultisigTx,
USDT_MULTISIG_CONFIG,
BRIDGE_MULTISIG_SPECIFIC_ADDRESSES,
} from '@pezkuwi/lib/multisig';
import {
listPendingOperations,
decodeAndVerifyCallData,
type PendingOperation,
} from '@pezkuwi/lib/multisig-operations';
interface MultisigOperationsProps {
specificAddresses?: Record<string, string>;
}
export const MultisigOperations: React.FC<MultisigOperationsProps> = ({
specificAddresses = BRIDGE_MULTISIG_SPECIFIC_ADDRESSES,
}) => {
const { t } = useTranslation();
const { api, isApiReady, selectedAccount, walletSource } = usePezkuwi();
const [multisigAddress, setMultisigAddress] = useState('');
const [otherSignatories, setOtherSignatories] = useState<string[]>([]);
const [operations, setOperations] = useState<PendingOperation[]>([]);
const [loading, setLoading] = useState(true);
const [processingHash, setProcessingHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [manualCallData, setManualCallData] = useState<Record<string, string>>({});
const [decodeErrors, setDecodeErrors] = useState<Record<string, string>>({});
const load = useCallback(async () => {
if (!api || !isApiReady) return;
setLoading(true);
setError(null);
try {
const allMembers = await getMultisigMembers(api, specificAddresses);
const multisigAddr = calculateMultisigAddress(allMembers);
setMultisigAddress(multisigAddr);
if (selectedAccount) {
setOtherSignatories(allMembers.filter((addr) => addr !== selectedAccount.address));
}
const ops = await listPendingOperations(api, multisigAddr);
setOperations(ops);
} catch (err) {
if (import.meta.env.DEV) console.error('Error loading pending multisig operations:', err);
setError(err instanceof Error ? err.message : 'Failed to load pending operations');
} finally {
setLoading(false);
}
}, [api, isApiReady, selectedAccount, specificAddresses]);
useEffect(() => {
load();
}, [load]);
const handleDecode = (callHash: string) => {
if (!api) return;
const hex = manualCallData[callHash];
if (!hex) return;
try {
const { call, description } = decodeAndVerifyCallData(api, hex, callHash);
setOperations((prev) =>
prev.map((op) =>
op.callHash === callHash ? { ...op, resolvedCall: call, description, isAutoMatched: false } : op
)
);
setDecodeErrors((prev) => ({ ...prev, [callHash]: '' }));
} catch (err) {
setDecodeErrors((prev) => ({
...prev,
[callHash]: err instanceof Error ? err.message : 'Failed to decode call data',
}));
}
};
const runExtrinsic = async (
callHash: string,
tx: ReturnType<typeof approveMultisigTx> | ReturnType<typeof cancelMultisigTx>,
successMessage: string
) => {
if (!api || !selectedAccount) return;
setProcessingHash(callHash);
setError(null);
setSuccess(null);
try {
const injector = await getSigner(selectedAccount.address, walletSource, api);
await new Promise<void>((resolve, reject) => {
tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, dispatchError }) => {
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve();
}
}
}).catch(reject);
});
setSuccess(successMessage);
await load();
} catch (err) {
if (import.meta.env.DEV) console.error('Multisig operation submission failed:', err);
setError(err instanceof Error ? err.message : 'Transaction failed');
} finally {
setProcessingHash(null);
}
};
const handleApprove = (op: PendingOperation) => {
if (!api || !op.resolvedCall) return;
const tx = approveMultisigTx(api, op.resolvedCall, otherSignatories, op.when, op.threshold);
runExtrinsic(op.callHash, tx, 'Approval submitted successfully.');
};
const handleReject = (op: PendingOperation) => {
if (!api) return;
const tx = cancelMultisigTx(api, op.callHash, otherSignatories, op.when, op.threshold);
runExtrinsic(op.callHash, tx, 'Operation cancelled successfully.');
};
if (loading) {
return (
<Card className="p-6 bg-gray-800/50 border-gray-700">
<div className="flex items-center justify-center py-8">
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
</div>
</Card>
);
}
return (
<Card className="p-6 bg-gray-800/50 border-gray-700">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Shield className="h-6 w-6 text-blue-400" />
<div>
<h3 className="text-lg font-bold text-white">{t('multisigOps.title')}</h3>
<p className="text-sm text-gray-400">
{t('multisigOps.subtitle', { count: operations.length })}
</p>
</div>
</div>
<Button variant="outline" size="sm" onClick={load} className="border-gray-700">
<RefreshCw className="h-4 w-4 mr-2" />
{t('multisigOps.refresh')}
</Button>
</div>
{error && (
<Alert className="mb-4 bg-red-900/20 border-red-500">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="mb-4 bg-green-900/20 border-green-500">
<CheckCircle2 className="h-4 w-4" />
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
{operations.length === 0 ? (
<div className="text-center py-8 text-gray-400">{t('multisigOps.empty')}</div>
) : (
<div className="space-y-4">
{operations.map((op) => {
const alreadyApproved = selectedAccount ? op.approvals.includes(selectedAccount.address) : false;
const canReject = selectedAccount ? op.depositor === selectedAccount.address : false;
const isProcessing = processingHash === op.callHash;
return (
<div key={op.callHash} className="p-4 bg-gray-900/30 rounded-lg space-y-3">
<div className="flex items-start justify-between gap-4">
<div>
<p className="font-semibold text-white">{op.description}</p>
<code className="text-xs text-gray-500 font-mono">
{op.callHash.slice(0, 10)}...{op.callHash.slice(-8)}
</code>
</div>
<Badge
variant={op.approvalsCount >= op.threshold ? 'default' : 'outline'}
className="whitespace-nowrap"
>
{op.approvalsCount}/{op.threshold} {t('multisigOps.approvals')}
</Badge>
</div>
<div className="text-xs text-gray-400 space-y-1">
<p>
{t('multisigOps.proposedBy')}:{' '}
<code className="font-mono">
{op.depositor.slice(0, 8)}...{op.depositor.slice(-6)}
</code>
</p>
{alreadyApproved && (
<p className="text-green-400 flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" /> {t('multisigOps.youApproved')}
</p>
)}
</div>
{!op.resolvedCall && (
<div className="space-y-2">
<p className="text-xs text-yellow-500">{t('multisigOps.unknownCallHint')}</p>
<Textarea
placeholder={t('multisigOps.pasteCallData')}
value={manualCallData[op.callHash] ?? ''}
onChange={(e) =>
setManualCallData((prev) => ({ ...prev, [op.callHash]: e.target.value }))
}
className="bg-gray-800 border-gray-700 text-white text-xs font-mono"
rows={2}
/>
{decodeErrors[op.callHash] && (
<p className="text-xs text-red-400">{decodeErrors[op.callHash]}</p>
)}
<Button
size="sm"
variant="outline"
onClick={() => handleDecode(op.callHash)}
disabled={!manualCallData[op.callHash]}
>
{t('multisigOps.decode')}
</Button>
</div>
)}
<div className="flex gap-2">
<Button
size="sm"
onClick={() => handleApprove(op)}
disabled={!op.resolvedCall || alreadyApproved || isProcessing || !selectedAccount}
className="bg-green-600 hover:bg-green-700"
>
{isProcessing ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="h-4 w-4 mr-2" />
)}
{t('multisigOps.approve')}
</Button>
{canReject && (
<Button
size="sm"
variant="destructive"
onClick={() => handleReject(op)}
disabled={isProcessing}
>
<XCircle className="h-4 w-4 mr-2" />
{t('multisigOps.reject')}
</Button>
)}
</div>
</div>
);
})}
</div>
)}
<Alert className="mt-6 bg-blue-900/20 border-blue-500">
<Shield className="h-4 w-4" />
<AlertDescription className="text-sm">
{t('multisigOps.thresholdNote', {
threshold: USDT_MULTISIG_CONFIG.threshold,
total: USDT_MULTISIG_CONFIG.members.length,
})}
</AlertDescription>
</Alert>
{multisigAddress && (
<p className="mt-4 text-xs text-gray-500 text-center font-mono">{multisigAddress}</p>
)}
</Card>
);
};
+77 -5
View File
@@ -4,11 +4,16 @@ import { useAuth } from '@/contexts/AuthContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Loader2, Wallet } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { isMultisigMember, BRIDGE_MULTISIG_SPECIFIC_ADDRESSES } from '@pezkuwi/lib/multisig';
interface ProtectedRouteProps {
children: React.ReactNode;
requireAdmin?: boolean;
allowTelegramSession?: boolean;
/** Gates access to the connected account being one of the 5 real USDT bridge multisig
* signatories (checked live against chain state, not a decorative badge like the
* isMultisigMember usage elsewhere in the app before this). */
requireMultisigMember?: boolean;
}
// Check if valid telegram session exists
@@ -32,14 +37,48 @@ function getTelegramSession(): { telegram_id: string; wallet_address: string; us
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requireAdmin = false,
allowTelegramSession = false
allowTelegramSession = false,
requireMultisigMember = false
}) => {
const { user, loading, isAdmin } = useAuth();
const { selectedAccount, connectWallet } = usePezkuwi();
const { api, isApiReady, selectedAccount, connectWallet } = usePezkuwi();
const [walletRestoreChecked, setWalletRestoreChecked] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0);
const [multisigCheckDone, setMultisigCheckDone] = useState(false);
const [isSigner, setIsSigner] = useState(false);
const telegramSession = allowTelegramSession ? getTelegramSession() : null;
// Live on-chain check - this is a REAL gate (unlike the isMultisigMember badge elsewhere in
// the app, which is purely decorative and gates nothing).
useEffect(() => {
if (!requireMultisigMember) return;
if (!api || !isApiReady || !selectedAccount) {
setMultisigCheckDone(false);
return;
}
let cancelled = false;
setMultisigCheckDone(false);
isMultisigMember(api, selectedAccount.address, BRIDGE_MULTISIG_SPECIFIC_ADDRESSES)
.then((result) => {
if (!cancelled) {
setIsSigner(result);
setMultisigCheckDone(true);
}
})
.catch(() => {
if (!cancelled) {
setIsSigner(false);
setMultisigCheckDone(true);
}
});
return () => {
cancelled = true;
};
}, [requireMultisigMember, api, isApiReady, selectedAccount]);
// Listen for wallet changes
useEffect(() => {
const handleWalletChange = () => {
@@ -81,8 +120,8 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
);
}
// For admin routes, require wallet connection
if (requireAdmin && !selectedAccount) {
// For admin/multisig-gated routes, require wallet connection
if ((requireAdmin || requireMultisigMember) && !selectedAccount) {
const handleConnect = async () => {
await connectWallet();
// Event is automatically dispatched by handleSetSelectedAccount wrapper
@@ -94,7 +133,9 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
<Wallet className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-white mb-2">Connect Your Wallet</h2>
<p className="text-gray-400 mb-6">
Admin panel requires wallet authentication. Please connect your wallet to continue.
{requireMultisigMember
? 'This page requires connecting one of the USDT bridge multisig signer accounts.'
: 'Admin panel requires wallet authentication. Please connect your wallet to continue.'}
</p>
<Button onClick={handleConnect} size="lg" className="bg-green-600 hover:bg-green-700">
<Wallet className="mr-2 h-5 w-5" />
@@ -127,5 +168,36 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
);
}
if (requireMultisigMember) {
if (!multisigCheckDone) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin text-green-500 mx-auto mb-4" />
<p className="text-gray-400">Verifying multisig signer status on-chain...</p>
</div>
</div>
);
}
if (!isSigner) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="text-center max-w-md">
<div className="text-red-500 text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-white mb-2">Access Denied</h2>
<p className="text-gray-400 mb-4">
Your wallet ({selectedAccount?.address.slice(0, 8)}...) is not one of the 5 USDT
bridge multisig signatories.
</p>
<p className="text-sm text-gray-500">
Only Serok, SerokiMeclise, Xezinedar, Noter, and Berdevk can access this page.
</p>
</div>
</div>
);
}
}
return <>{children}</>;
};
+42 -32
View File
@@ -14,9 +14,10 @@ import {
getWithdrawalTier,
formatDelay,
formatWUSDT,
createWithdrawalInitiationTx,
deriveWithdrawalDestination,
} from '@pezkuwi/lib/usdt';
import { isMultisigMember } from '@pezkuwi/lib/multisig';
import { ASSET_IDS } from '@pezkuwi/lib/wallet';
interface USDTBridgeProps {
isOpen: boolean;
@@ -35,7 +36,6 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
const [depositAmount, setDepositAmount] = useState('');
const [withdrawAmount, setWithdrawAmount] = useState('');
const [withdrawAddress, setWithdrawAddress] = useState(''); // Bank account or crypto address
const [wusdtBalance, setWusdtBalance] = useState(0);
const [isMultisigMemberState, setIsMultisigMemberState] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -88,7 +88,14 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
}
};
// Handle withdrawal (burn wUSDT)
// Handle withdrawal - the user transfers their OWN wUSDT to the multisig custody account.
// This used to call `assets.burn` directly, signed by the user - but burn requires Admin
// origin (the multisig), so that call has always failed on-chain with NoPermission. The bug
// went unnoticed because the callback only checked `status.isFinalized`, and a FAILED
// dispatch is still included/finalized as a valid extrinsic - so every withdrawal reported
// "success" while silently doing nothing. usdt-bridge's relayer (listen_withdrawals) actually
// watches for exactly this transfer-to-custody event and records it as pending_multisig
// approval; burning happens later, once the multisig actually releases real USDT.
const handleWithdrawal = async () => {
if (!api || !selectedAccount) return;
@@ -104,34 +111,40 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
return;
}
if (!withdrawAddress) {
setError(t('bridge.noAddress'));
return;
}
setIsLoading(true);
setError(null);
setSuccess(null);
try {
const injector = await getSigner(selectedAccount.address, walletSource, api);
const transferTx = createWithdrawalInitiationTx(api, amount);
// Burn wUSDT
const amountBN = BigInt(Math.floor(amount * 1e6)); // 6 decimals
const burnTx = api.tx.assets.burn(ASSET_IDS.WUSDT, selectedAccount.address, amountBN.toString());
await burnTx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status }) => {
if (status.isFinalized) {
const delay = calculateWithdrawalDelay(amount);
setSuccess(
t('bridge.withdrawSuccess', { address: withdrawAddress, delay: formatDelay(delay) })
);
setWithdrawAmount('');
setWithdrawAddress('');
refreshBalances();
setIsLoading(false);
}
await new Promise<void>((resolve, reject) => {
transferTx
.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, dispatchError }) => {
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve();
}
}
})
.catch(reject);
});
const delay = calculateWithdrawalDelay(amount);
setSuccess(
t('bridge.withdrawSuccess', { address: withdrawDestination, delay: formatDelay(delay) })
);
setWithdrawAmount('');
refreshBalances();
setIsLoading(false);
} catch (err) {
if (import.meta.env.DEV) console.error('Withdrawal error:', err);
setError(err instanceof Error ? err.message : 'Withdrawal failed');
@@ -143,6 +156,7 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
const withdrawalTier = withdrawAmount ? getWithdrawalTier(parseFloat(withdrawAmount)) : null;
const withdrawalDelay = withdrawAmount ? calculateWithdrawalDelay(parseFloat(withdrawAmount)) : 0;
const withdrawDestination = selectedAccount ? deriveWithdrawalDestination(selectedAccount.address) : '';
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
@@ -299,14 +313,10 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
<label className="block text-sm font-medium text-gray-300 mb-2">
{t('bridge.withdrawAddress')}
</label>
<input
type="text"
value={withdrawAddress}
onChange={(e) => setWithdrawAddress(e.target.value)}
placeholder={t('bridge.addressPlaceholder')}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 placeholder:text-gray-500 placeholder:opacity-50"
disabled={isLoading}
/>
<div className="w-full bg-gray-800/60 border border-gray-700 rounded-lg px-4 py-3">
<code className="text-sm text-gray-300 font-mono break-all">{withdrawDestination}</code>
</div>
<p className="text-xs text-gray-500 mt-1">{t('bridge.withdrawAddressAutoNote')}</p>
</div>
{withdrawAmount && parseFloat(withdrawAmount) > 0 && (
@@ -333,7 +343,7 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
<Button
onClick={handleWithdrawal}
disabled={isLoading || !withdrawAmount || !withdrawAddress}
disabled={isLoading || !withdrawAmount || !selectedAccount}
className="w-full bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 h-12"
>
{isLoading ? (
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ArrowLeft } from 'lucide-react';
import { MultisigOperations } from '@/components/MultisigOperations';
const MultisigOperationsPage = () => {
const navigate = useNavigate();
const { t } = useTranslation();
return (
<div className="min-h-screen bg-gray-950 pt-24 pb-12">
<div className="container mx-auto px-4 py-8 relative max-w-3xl">
<button
onClick={() => navigate('/wallet')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors flex items-center gap-2"
>
<ArrowLeft className="w-5 h-5" />
<span>{t('multisigOps.backToWallet')}</span>
</button>
<div className="pt-16">
<MultisigOperations />
</div>
</div>
</div>
);
};
export default MultisigOperationsPage;
+3 -9
View File
@@ -4,13 +4,7 @@ import { ArrowLeft, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ReservesDashboard } from '@/components/ReservesDashboard';
import { USDTBridge } from '@/components/USDTBridge';
// USDT Treasury Multisig Member Addresses
const SPECIFIC_ADDRESSES = {
// Non-unique roles - manually specified
Noter: '5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3',
Berdevk: '5F4V6dzpe72dE2C7YN3y7VGznMTWPFeSKL3ANhp4XasXjfvj',
};
import { BRIDGE_MULTISIG_SPECIFIC_ADDRESSES } from '@pezkuwi/lib/multisig';
const ReservesDashboardPage = () => {
const navigate = useNavigate();
@@ -42,7 +36,7 @@ const ReservesDashboardPage = () => {
{/* Main Content */}
<ReservesDashboard
specificAddresses={SPECIFIC_ADDRESSES}
specificAddresses={BRIDGE_MULTISIG_SPECIFIC_ADDRESSES}
offChainReserveAmount={offChainReserve}
/>
@@ -50,7 +44,7 @@ const ReservesDashboardPage = () => {
<USDTBridge
isOpen={isBridgeOpen}
onClose={() => setIsBridgeOpen(false)}
specificAddresses={SPECIFIC_ADDRESSES}
specificAddresses={BRIDGE_MULTISIG_SPECIFIC_ADDRESSES}
/>
</div>
</div>
+11 -2
View File
@@ -24,8 +24,17 @@
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
"@/*": ["./src/*"],
"@pezkuwi/i18n": ["../shared/i18n"],
"@pezkuwi/lib": ["../shared/lib"],
"@pezkuwi/lib/*": ["../shared/lib/*"],
"@pezkuwi/utils": ["../shared/utils"],
"@pezkuwi/theme": ["../shared/theme"],
"@pezkuwi/types": ["../shared/types"],
"@pezkuwi/components/*": ["../shared/components/*"],
"@shared/*": ["../shared/*"]
},
"types": ["vite/client"]
},
"include": ["src", "../shared"]
}