mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-25 01:47:55 +00:00
feat: add XCM teleport and CI/CD deployment workflow
Features: - Add XCMTeleportModal for cross-chain HEZ transfers - Support Asset Hub and People Chain teleports - Add "Fund Fees" button with user-friendly tooltips - Use correct XCM V3 format with teyrchain junction Fixes: - Fix PEZ transfer to use Asset Hub API - Silence unnecessary pallet availability warnings - Fix transaction loading performance (10 blocks limit) - Remove Supabase admin_roles dependency CI/CD: - Add auto-deploy to VPS on main branch push - Add version bumping on deploy - Upload build artifacts for deployment
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Wallet, TrendingUp, ArrowDownRight, RefreshCw, Award, Plus, Coins, Send, Shield, Users } from 'lucide-react';
|
||||
import { Wallet, TrendingUp, ArrowDownRight, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
import { AddTokenModal } from './AddTokenModal';
|
||||
import { TransferModal } from './TransferModal';
|
||||
import { XCMTeleportModal } from './XCMTeleportModal';
|
||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||
|
||||
interface TokenBalance {
|
||||
@@ -18,7 +19,7 @@ interface TokenBalance {
|
||||
}
|
||||
|
||||
export const AccountBalance: React.FC = () => {
|
||||
const { api, assetHubApi, isApiReady, isAssetHubReady, selectedAccount } = usePezkuwi();
|
||||
const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const [balance, setBalance] = useState<{
|
||||
free: string;
|
||||
reserved: string;
|
||||
@@ -28,6 +29,9 @@ export const AccountBalance: React.FC = () => {
|
||||
reserved: '0',
|
||||
total: '0',
|
||||
});
|
||||
// HEZ balances on different chains
|
||||
const [assetHubHezBalance, setAssetHubHezBalance] = useState<string>('0');
|
||||
const [peopleHezBalance, setPeopleHezBalance] = useState<string>('0');
|
||||
const [pezBalance, setPezBalance] = useState<string>('0');
|
||||
const [usdtBalance, setUsdtBalance] = useState<string>('0');
|
||||
const [hezUsdPrice, setHezUsdPrice] = useState<number>(0);
|
||||
@@ -44,6 +48,7 @@ export const AccountBalance: React.FC = () => {
|
||||
const [otherTokens, setOtherTokens] = useState<TokenBalance[]>([]);
|
||||
const [isAddTokenModalOpen, setIsAddTokenModalOpen] = useState(false);
|
||||
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
|
||||
const [isXCMTeleportModalOpen, setIsXCMTeleportModalOpen] = useState(false);
|
||||
const [selectedTokenForTransfer, setSelectedTokenForTransfer] = useState<TokenBalance | null>(null);
|
||||
const [customTokenIds, setCustomTokenIds] = useState<number[]>(() => {
|
||||
const stored = localStorage.getItem('customTokenIds');
|
||||
@@ -319,6 +324,36 @@ export const AccountBalance: React.FC = () => {
|
||||
total: totalTokens,
|
||||
});
|
||||
|
||||
// Fetch HEZ balance on Asset Hub (for PEZ transfer fees)
|
||||
try {
|
||||
if (assetHubApi && isAssetHubReady) {
|
||||
const { data: assetHubBalanceData } = await assetHubApi.query.system.account(selectedAccount.address);
|
||||
const assetHubFree = assetHubBalanceData.free.toString();
|
||||
const assetHubHezTokens = (parseInt(assetHubFree) / divisor).toFixed(4);
|
||||
setAssetHubHezBalance(assetHubHezTokens);
|
||||
} else {
|
||||
setAssetHubHezBalance('0.0000');
|
||||
}
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Failed to fetch Asset Hub HEZ balance:', error);
|
||||
setAssetHubHezBalance('0.0000');
|
||||
}
|
||||
|
||||
// Fetch HEZ balance on People Chain (for identity/KYC fees)
|
||||
try {
|
||||
if (peopleApi && isPeopleReady) {
|
||||
const { data: peopleBalanceData } = await peopleApi.query.system.account(selectedAccount.address);
|
||||
const peopleFree = peopleBalanceData.free.toString();
|
||||
const peopleHezTokens = (parseInt(peopleFree) / divisor).toFixed(4);
|
||||
setPeopleHezBalance(peopleHezTokens);
|
||||
} else {
|
||||
setPeopleHezBalance('0.0000');
|
||||
}
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Failed to fetch People Chain HEZ balance:', error);
|
||||
setPeopleHezBalance('0.0000');
|
||||
}
|
||||
|
||||
// Fetch PEZ balance (Asset ID: 1) from Asset Hub
|
||||
try {
|
||||
if (assetHubApi && isAssetHubReady) {
|
||||
@@ -538,59 +573,107 @@ export const AccountBalance: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* HEZ Balance Card */}
|
||||
{/* HEZ Balance Card - Multi-Chain */}
|
||||
<Card className="bg-gradient-to-br from-green-900/30 to-yellow-900/30 border-green-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/tokens/HEZ.png" alt="HEZ" className="w-10 h-10 rounded-full" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
HEZ Balance
|
||||
</CardTitle>
|
||||
<div>
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
HEZ Balance
|
||||
</CardTitle>
|
||||
<div className="text-xs text-gray-500">Multi-Chain Overview</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsXCMTeleportModalOpen(true)}
|
||||
className="border-yellow-500/50 text-yellow-400 hover:bg-yellow-500/10 group relative"
|
||||
title="Send HEZ to teyrcahins for transaction fees"
|
||||
>
|
||||
<Fuel className="w-4 h-4 mr-1" />
|
||||
Fund Fees
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
|
||||
Send HEZ to Asset Hub / People Chain
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={fetchBalance}
|
||||
disabled={isLoading}
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={fetchBalance}
|
||||
disabled={isLoading}
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Total HEZ */}
|
||||
<div>
|
||||
<div className="text-4xl font-bold text-white mb-1">
|
||||
{isLoading ? '...' : balance.total}
|
||||
{isLoading ? '...' : (parseFloat(balance.total) + parseFloat(assetHubHezBalance) + parseFloat(peopleHezBalance)).toFixed(4)}
|
||||
<span className="text-2xl text-gray-400 ml-2">HEZ</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{hezUsdPrice > 0
|
||||
? `≈ $${(parseFloat(balance.total) * hezUsdPrice).toFixed(2)} USD`
|
||||
? `≈ $${((parseFloat(balance.total) + parseFloat(assetHubHezBalance) + parseFloat(peopleHezBalance)) * hezUsdPrice).toFixed(2)} USD (Total across all chains)`
|
||||
: 'Price loading...'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrendingUp className="w-4 h-4 text-green-400" />
|
||||
<span className="text-xs text-gray-400">Transferable</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-white">
|
||||
{balance.free} HEZ
|
||||
{/* Chain Balances */}
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{/* Relay Chain (Main) */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-3 border border-green-500/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm text-gray-300">Pezkuwi (Relay Chain)</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-white">{balance.free} HEZ</div>
|
||||
<div className="text-xs text-gray-500">Reserved: {balance.reserved}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ArrowDownRight className="w-4 h-4 text-yellow-400" />
|
||||
<span className="text-xs text-gray-400">Reserved</span>
|
||||
{/* Asset Hub */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-3 border border-blue-500/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
|
||||
<span className="text-sm text-gray-300">Pezkuwi Asset Hub</span>
|
||||
<span className="text-xs text-gray-500">(PEZ fees)</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-white">{assetHubHezBalance} HEZ</div>
|
||||
{parseFloat(assetHubHezBalance) < 0.1 && (
|
||||
<div className="text-xs text-yellow-400">⚠️ Low for fees</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-white">
|
||||
{balance.reserved} HEZ
|
||||
</div>
|
||||
|
||||
{/* People Chain */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-3 border border-purple-500/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-purple-500"></div>
|
||||
<span className="text-sm text-gray-300">Pezkuwi People</span>
|
||||
<span className="text-xs text-gray-500">(Identity fees)</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-semibold text-white">{peopleHezBalance} HEZ</div>
|
||||
{parseFloat(peopleHezBalance) < 0.1 && (
|
||||
<div className="text-xs text-yellow-400">⚠️ Low for fees</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,11 +684,26 @@ export const AccountBalance: React.FC = () => {
|
||||
{/* PEZ Balance Card */}
|
||||
<Card className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 border-blue-500/30">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/tokens/PEZ.png" alt="PEZ" className="w-10 h-10 rounded-full" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
PEZ Token Balance
|
||||
</CardTitle>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<img src="/tokens/PEZ.png" alt="PEZ" className="w-10 h-10 rounded-full flex-shrink-0" />
|
||||
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
||||
PEZ Balance
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setIsXCMTeleportModalOpen(true)}
|
||||
className="border-yellow-500/50 text-yellow-400 hover:bg-yellow-500/10 group relative"
|
||||
title="Send HEZ to Asset Hub for transaction fees"
|
||||
>
|
||||
<Fuel className="w-4 h-4 mr-1" />
|
||||
Add Fees
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">
|
||||
Send HEZ for PEZ transfer fees
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -620,7 +718,7 @@ export const AccountBalance: React.FC = () => {
|
||||
: 'Price loading...'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Governance & Rewards Token
|
||||
Governance & Rewards Token (on Asset Hub)
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -848,6 +946,12 @@ export const AccountBalance: React.FC = () => {
|
||||
}}
|
||||
selectedAsset={selectedTokenForTransfer}
|
||||
/>
|
||||
|
||||
{/* XCM Teleport Modal */}
|
||||
<XCMTeleportModal
|
||||
isOpen={isXCMTeleportModalOpen}
|
||||
onClose={() => setIsXCMTeleportModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,25 +53,10 @@ const AppLayout: React.FC = () => {
|
||||
useWallet();
|
||||
const [, _setIsAdmin] = useState(false);
|
||||
|
||||
// Check if user is admin
|
||||
// Admin status is handled by AuthContext via wallet whitelist
|
||||
// Supabase admin_roles is optional (table may not exist)
|
||||
React.useEffect(() => {
|
||||
const checkAdminStatus = async () => {
|
||||
if (user) {
|
||||
const { data, error } = await supabase
|
||||
.from('admin_roles')
|
||||
.select('role')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
if (import.meta.env.DEV) console.warn('Admin check error:', error);
|
||||
}
|
||||
_setIsAdmin(!!data);
|
||||
} else {
|
||||
_setIsAdmin(false);
|
||||
}
|
||||
};
|
||||
checkAdminStatus();
|
||||
_setIsAdmin(false); // Admin status managed by AuthContext
|
||||
}, [user]);
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-white">
|
||||
|
||||
@@ -66,7 +66,7 @@ const TOKENS: Token[] = [
|
||||
];
|
||||
|
||||
export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, selectedAsset }) => {
|
||||
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
||||
const { api, assetHubApi, isApiReady, isAssetHubReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [selectedToken, setSelectedToken] = useState<TokenType>('HEZ');
|
||||
@@ -97,6 +97,17 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if PEZ transfer but Asset Hub not ready
|
||||
const isPezTransfer = currentToken.symbol === 'PEZ' || currentToken.assetId === 1;
|
||||
if (isPezTransfer && (!assetHubApi || !isAssetHubReady)) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Asset Hub connection not ready. PEZ is on Asset Hub.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!recipient || !amount) {
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -118,14 +129,20 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
const amountInSmallestUnit = BigInt(parseFloat(amount) * Math.pow(10, currentToken.decimals));
|
||||
|
||||
let transfer;
|
||||
let targetApi = api; // Default to main chain API
|
||||
|
||||
// Create appropriate transfer transaction based on token type
|
||||
// wHEZ uses native token transfer (balances pallet), all others use assets pallet
|
||||
// HEZ uses native token transfer (balances pallet on main chain)
|
||||
// PEZ uses assets pallet on Asset Hub (asset ID: 1)
|
||||
if (currentToken.assetId === undefined || (selectedToken === 'HEZ' && !selectedAsset)) {
|
||||
// Native HEZ token transfer
|
||||
// Native HEZ token transfer on main chain
|
||||
transfer = api.tx.balances.transferKeepAlive(recipient, amountInSmallestUnit.toString());
|
||||
} else if (isPezTransfer) {
|
||||
// PEZ transfer on Asset Hub (asset ID: 1)
|
||||
targetApi = assetHubApi!;
|
||||
transfer = assetHubApi!.tx.assets.transfer(currentToken.assetId, recipient, amountInSmallestUnit.toString());
|
||||
} else {
|
||||
// Asset token transfer (wHEZ, PEZ, wUSDT, etc.)
|
||||
// Other asset token transfers on main chain
|
||||
transfer = api.tx.assets.transfer(currentToken.assetId, recipient, amountInSmallestUnit.toString());
|
||||
}
|
||||
|
||||
@@ -149,7 +166,7 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
let errorMessage = 'Transaction failed';
|
||||
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
const decoded = targetApi.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { ArrowDown, Loader2, CheckCircle, XCircle, Info } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
type TargetChain = 'asset-hub' | 'people';
|
||||
|
||||
interface ChainInfo {
|
||||
id: TargetChain;
|
||||
name: string;
|
||||
description: string;
|
||||
teyrchainId: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const TARGET_CHAINS: ChainInfo[] = [
|
||||
{
|
||||
id: 'asset-hub',
|
||||
name: 'Pezkuwi Asset Hub',
|
||||
description: 'For PEZ token transfers',
|
||||
teyrchainId: 1000,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'people',
|
||||
name: 'Pezkuwi People',
|
||||
description: 'For identity & citizenship',
|
||||
teyrchainId: 1004,
|
||||
color: 'purple',
|
||||
},
|
||||
];
|
||||
|
||||
interface XCMTeleportModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const XCMTeleportModal: React.FC<XCMTeleportModalProps> = ({ isOpen, onClose }) => {
|
||||
const { api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [targetChain, setTargetChain] = useState<TargetChain>('asset-hub');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [isTransferring, setIsTransferring] = useState(false);
|
||||
const [txStatus, setTxStatus] = useState<'idle' | 'signing' | 'pending' | 'success' | 'error'>('idle');
|
||||
const [txHash, setTxHash] = useState('');
|
||||
const [relayBalance, setRelayBalance] = useState<string>('0');
|
||||
const [assetHubBalance, setAssetHubBalance] = useState<string>('0');
|
||||
const [peopleBalance, setPeopleBalance] = useState<string>('0');
|
||||
|
||||
const selectedChain = TARGET_CHAINS.find(c => c.id === targetChain)!;
|
||||
|
||||
// Fetch balances
|
||||
useEffect(() => {
|
||||
const fetchBalances = async () => {
|
||||
if (!selectedAccount?.address) return;
|
||||
|
||||
// Relay chain balance
|
||||
if (api && isApiReady) {
|
||||
try {
|
||||
const accountInfo = await api.query.system.account(selectedAccount.address);
|
||||
const free = (accountInfo as any).data.free.toString();
|
||||
const balanceNum = Number(free) / 1e12;
|
||||
setRelayBalance(balanceNum.toFixed(4));
|
||||
} catch (err) {
|
||||
console.error('Error fetching relay balance:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Asset Hub balance
|
||||
if (assetHubApi && isAssetHubReady) {
|
||||
try {
|
||||
const accountInfo = await assetHubApi.query.system.account(selectedAccount.address);
|
||||
const free = (accountInfo as any).data.free.toString();
|
||||
const balanceNum = Number(free) / 1e12;
|
||||
setAssetHubBalance(balanceNum.toFixed(4));
|
||||
} catch (err) {
|
||||
console.error('Error fetching Asset Hub balance:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// People chain balance
|
||||
if (peopleApi && isPeopleReady) {
|
||||
try {
|
||||
const accountInfo = await peopleApi.query.system.account(selectedAccount.address);
|
||||
const free = (accountInfo as any).data.free.toString();
|
||||
const balanceNum = Number(free) / 1e12;
|
||||
setPeopleBalance(balanceNum.toFixed(4));
|
||||
} catch (err) {
|
||||
console.error('Error fetching People chain balance:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
fetchBalances();
|
||||
}
|
||||
}, [api, assetHubApi, peopleApi, isApiReady, isAssetHubReady, isPeopleReady, selectedAccount, isOpen]);
|
||||
|
||||
const getTargetBalance = () => {
|
||||
return targetChain === 'asset-hub' ? assetHubBalance : peopleBalance;
|
||||
};
|
||||
|
||||
const handleTeleport = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Wallet not connected",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please enter a valid amount",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sendAmount = parseFloat(amount);
|
||||
const currentBalance = parseFloat(relayBalance);
|
||||
|
||||
if (sendAmount > currentBalance) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Insufficient balance on Relay Chain",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTransferring(true);
|
||||
setTxStatus('signing');
|
||||
|
||||
try {
|
||||
const { web3FromAddress } = await import('@pezkuwi/extension-dapp');
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
|
||||
// Convert to smallest unit (12 decimals)
|
||||
const amountInSmallestUnit = BigInt(Math.floor(parseFloat(amount) * 1e12));
|
||||
|
||||
// Get target teyrchain ID
|
||||
const targetTeyrchainId = selectedChain.teyrchainId;
|
||||
|
||||
// Destination: Target teyrchain
|
||||
const dest = {
|
||||
V3: {
|
||||
parents: 0,
|
||||
interior: {
|
||||
X1: { teyrchain: targetTeyrchainId }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Beneficiary: Same account on target chain
|
||||
const beneficiary = {
|
||||
V3: {
|
||||
parents: 0,
|
||||
interior: {
|
||||
X1: {
|
||||
accountid32: {
|
||||
network: null,
|
||||
id: api.createType('AccountId32', selectedAccount.address).toHex()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Assets: Native token (HEZ)
|
||||
const assets = {
|
||||
V3: [{
|
||||
id: {
|
||||
Concrete: {
|
||||
parents: 0,
|
||||
interior: 'Here'
|
||||
}
|
||||
},
|
||||
fun: {
|
||||
Fungible: amountInSmallestUnit.toString()
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
// Fee asset ID: Native HEZ token (VersionedAssetId format)
|
||||
const feeAssetId = {
|
||||
V3: {
|
||||
Concrete: {
|
||||
parents: 0,
|
||||
interior: 'Here'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const weightLimit = 'Unlimited';
|
||||
|
||||
// Create teleport transaction
|
||||
const tx = api.tx.xcmPallet.limitedTeleportAssets(
|
||||
dest,
|
||||
beneficiary,
|
||||
assets,
|
||||
feeAssetId,
|
||||
weightLimit
|
||||
);
|
||||
|
||||
setTxStatus('pending');
|
||||
|
||||
const unsub = await tx.signAndSend(
|
||||
selectedAccount.address,
|
||||
{ signer: injector.signer },
|
||||
({ status, events, dispatchError }) => {
|
||||
if (status.isInBlock) {
|
||||
if (import.meta.env.DEV) console.log(`XCM Teleport in block: ${status.asInBlock}`);
|
||||
setTxHash(status.asInBlock.toHex());
|
||||
}
|
||||
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Teleport failed';
|
||||
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs}`;
|
||||
}
|
||||
|
||||
setTxStatus('error');
|
||||
toast({
|
||||
title: "Teleport Failed",
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
setTxStatus('success');
|
||||
toast({
|
||||
title: "Teleport Successful!",
|
||||
description: `${amount} HEZ teleported to ${selectedChain.name}!`,
|
||||
});
|
||||
|
||||
// Reset after success
|
||||
setTimeout(() => {
|
||||
setAmount('');
|
||||
setTxStatus('idle');
|
||||
setTxHash('');
|
||||
onClose();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
setIsTransferring(false);
|
||||
unsub();
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Teleport error:', error);
|
||||
setTxStatus('error');
|
||||
setIsTransferring(false);
|
||||
|
||||
toast({
|
||||
title: "Teleport Failed",
|
||||
description: error instanceof Error ? error.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isTransferring) {
|
||||
setAmount('');
|
||||
setTxStatus('idle');
|
||||
setTxHash('');
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const setQuickAmount = (percent: number) => {
|
||||
const balance = parseFloat(relayBalance);
|
||||
if (balance > 0) {
|
||||
const quickAmount = (balance * percent / 100).toFixed(4);
|
||||
setAmount(quickAmount);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800 max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white flex items-center gap-2">
|
||||
<img src="/tokens/HEZ.png" alt="HEZ" className="w-6 h-6 rounded-full" />
|
||||
Teleport HEZ to Teyrchain
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Transfer HEZ from Pezkuwi (Relay Chain) to a teyrchain for transaction fees
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{txStatus === 'success' ? (
|
||||
<div className="py-8 text-center">
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Teleport Successful!</h3>
|
||||
<p className="text-gray-400 mb-4">{amount} HEZ sent to {selectedChain.name}</p>
|
||||
{txHash && (
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Transaction Hash</div>
|
||||
<div className="text-white font-mono text-xs break-all">{txHash}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : txStatus === 'error' ? (
|
||||
<div className="py-8 text-center">
|
||||
<XCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold text-white mb-2">Teleport Failed</h3>
|
||||
<p className="text-gray-400">Please try again</p>
|
||||
<Button
|
||||
onClick={() => setTxStatus('idle')}
|
||||
className="mt-4 bg-gray-800 hover:bg-gray-700"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Target Chain Selection */}
|
||||
<div>
|
||||
<Label className="text-white">Target Teyrchain</Label>
|
||||
<Select value={targetChain} onValueChange={(v) => setTargetChain(v as TargetChain)}>
|
||||
<SelectTrigger className="bg-gray-800 border-gray-700 text-white mt-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-gray-800 border-gray-700">
|
||||
{TARGET_CHAINS.map((chain) => (
|
||||
<SelectItem
|
||||
key={chain.id}
|
||||
value={chain.id}
|
||||
className="text-white hover:bg-gray-700 focus:bg-gray-700"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full bg-${chain.color}-500`}></div>
|
||||
<span>{chain.name}</span>
|
||||
<span className="text-gray-400 text-xs">- {chain.description}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Balance Display */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-4 space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<span className="text-sm text-gray-400">Pezkuwi (Relay Chain)</span>
|
||||
</div>
|
||||
<span className="text-white font-mono">{relayBalance} HEZ</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<ArrowDown className="w-5 h-5 text-yellow-500" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full bg-${selectedChain.color}-500`}></div>
|
||||
<span className="text-sm text-gray-400">{selectedChain.name}</span>
|
||||
</div>
|
||||
<span className="text-white font-mono">{getTargetBalance()} HEZ</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className={`bg-${selectedChain.color}-500/10 border border-${selectedChain.color}-500/30 rounded-lg p-3 flex gap-2`}>
|
||||
<Info className={`w-5 h-5 text-${selectedChain.color}-400 flex-shrink-0 mt-0.5`} />
|
||||
<p className={`text-${selectedChain.color}-400 text-sm`}>
|
||||
{selectedChain.description}. Teleport at least 0.1 HEZ for fees.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div>
|
||||
<Label htmlFor="amount" className="text-white">Amount (HEZ)</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="0.1"
|
||||
className="bg-gray-800 border-gray-700 text-white mt-2"
|
||||
disabled={isTransferring}
|
||||
/>
|
||||
|
||||
{/* Quick Amount Buttons */}
|
||||
<div className="flex gap-2 mt-2">
|
||||
{[10, 25, 50, 100].map((percent) => (
|
||||
<button
|
||||
key={percent}
|
||||
onClick={() => setQuickAmount(percent)}
|
||||
className="flex-1 py-1 px-2 text-xs bg-gray-800 hover:bg-gray-700 text-gray-400 rounded border border-gray-700"
|
||||
disabled={isTransferring}
|
||||
>
|
||||
{percent}%
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Messages */}
|
||||
{txStatus === 'signing' && (
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3">
|
||||
<p className="text-yellow-400 text-sm">
|
||||
Please sign the transaction in your wallet extension
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{txStatus === 'pending' && (
|
||||
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-3">
|
||||
<p className="text-blue-400 text-sm flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
XCM Teleport in progress... This may take a moment.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
onClick={handleTeleport}
|
||||
disabled={isTransferring || !amount || parseFloat(amount) <= 0}
|
||||
className="w-full bg-gradient-to-r from-green-600 to-yellow-400 hover:opacity-90"
|
||||
>
|
||||
{isTransferring ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing XCM...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Teleport HEZ to {selectedChain.name}
|
||||
<ArrowDown className="w-4 h-4 ml-2" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user