mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-22 10:15:51 +00:00
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:
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}</>;
|
||||
};
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user