feat: add bidirectional HEZ teleport (Relay ↔ Parachain)

This commit is contained in:
2026-02-07 21:31:25 +03:00
parent 4d4eb72722
commit e76bec3284
3 changed files with 256 additions and 124 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pezkuwi-telegram-miniapp",
"version": "1.0.159",
"version": "1.0.160",
"type": "module",
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
"author": "Pezkuwichain Team",
+252 -120
View File
@@ -1,14 +1,25 @@
/**
* Fund Fees Modal - XCM Teleport HEZ to Teyerchains
* Allows users to transfer HEZ from relay chain to Asset Hub or People chain for fees
* Fund Fees Modal - XCM Teleport HEZ between Relay Chain and Parachains
* Supports bidirectional teleport: Relay ↔ Asset Hub / People Chain
*/
import { useState, useEffect } from 'react';
import { X, ArrowDown, Loader2, CheckCircle, AlertCircle, Fuel, Info } from 'lucide-react';
import {
X,
ArrowDown,
ArrowUp,
Loader2,
CheckCircle,
AlertCircle,
Fuel,
Info,
ArrowLeftRight,
} from 'lucide-react';
import { useWallet } from '@/contexts/WalletContext';
import { useTelegram } from '@/hooks/useTelegram';
type TargetChain = 'asset-hub' | 'people';
type TeleportDirection = 'to-parachain' | 'to-relay';
interface ChainInfo {
id: TargetChain;
@@ -45,6 +56,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
const { hapticImpact, showAlert } = useTelegram();
const [targetChain, setTargetChain] = useState<TargetChain>('asset-hub');
const [direction, setDirection] = useState<TeleportDirection>('to-parachain');
const [amount, setAmount] = useState('');
const [isTransferring, setIsTransferring] = useState(false);
const [txStatus, setTxStatus] = useState<'idle' | 'signing' | 'pending' | 'success' | 'error'>(
@@ -56,6 +68,38 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
const selectedChain = TARGET_CHAINS.find((c) => c.id === targetChain) || TARGET_CHAINS[0];
// Get source balance based on direction
const getSourceBalance = () => {
if (direction === 'to-parachain') {
return relayBalance;
}
return targetChain === 'asset-hub' ? assetHubBalance : peopleBalance;
};
// Get destination balance based on direction
const getDestBalance = () => {
if (direction === 'to-parachain') {
return targetChain === 'asset-hub' ? assetHubBalance : peopleBalance;
}
return relayBalance;
};
// Get source chain name
const getSourceChainName = () => {
if (direction === 'to-parachain') {
return 'Relay Chain';
}
return selectedChain.name;
};
// Get destination chain name
const getDestChainName = () => {
if (direction === 'to-parachain') {
return selectedChain.name;
}
return 'Relay Chain';
};
// Fetch balances
useEffect(() => {
const fetchBalances = async () => {
@@ -121,12 +165,8 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
}
}, [api, assetHubApi, peopleApi, address, isOpen]);
const getTargetBalance = () => {
return targetChain === 'asset-hub' ? assetHubBalance : peopleBalance;
};
const handleTeleport = async () => {
if (!api || !address || !keypair) {
if (!address || !keypair) {
showAlert('Cizdan girêdayî nîne');
return;
}
@@ -136,19 +176,34 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
return;
}
if (relayBalance === '--') {
showAlert('Relay Chain girêdayî nîne');
const sourceBalance = getSourceBalance();
if (sourceBalance === '--') {
showAlert('Zincîr girêdayî nîne');
return;
}
const sendAmount = parseFloat(amount);
const currentBalance = parseFloat(relayBalance);
const currentBalance = parseFloat(sourceBalance);
if (sendAmount > currentBalance) {
showAlert('Bakiye têrê nake');
return;
}
// Get the appropriate API based on direction
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let sourceApi: any;
if (direction === 'to-parachain') {
sourceApi = api;
} else {
sourceApi = targetChain === 'asset-hub' ? assetHubApi : peopleApi;
}
if (!sourceApi) {
showAlert('API girêdayî nîne');
return;
}
setIsTransferring(true);
setTxStatus('signing');
hapticImpact('medium');
@@ -157,103 +212,156 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
// Convert to smallest unit (12 decimals)
const amountInSmallestUnit = BigInt(Math.floor(parseFloat(amount) * 1e12));
// Get target teyrchain ID
const targetTeyrchainId = selectedChain.teyrchainId;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let dest: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let beneficiary: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let assets: any;
// Destination: Target teyrchain
const dest = {
V3: {
parents: 0,
interior: {
X1: { teyrchain: targetTeyrchainId },
},
},
};
if (direction === 'to-parachain') {
// Relay Chain → Parachain
const targetTeyrchainId = selectedChain.teyrchainId;
// Beneficiary: Same account on target chain
const beneficiary = {
V3: {
parents: 0,
interior: {
X1: {
accountid32: {
network: null,
id: api.createType('AccountId32', 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
const feeAssetId = {
V3: {
Concrete: {
dest = {
V3: {
parents: 0,
interior: {
X1: { Parachain: targetTeyrchainId },
},
},
};
beneficiary = {
V3: {
parents: 0,
interior: {
X1: {
AccountId32: {
network: null,
id: sourceApi.createType('AccountId32', address).toHex(),
},
},
},
},
};
// Native token on relay chain
assets = {
V3: [
{
id: {
Concrete: {
parents: 0,
interior: 'Here',
},
},
fun: {
Fungible: amountInSmallestUnit.toString(),
},
},
],
};
} else {
// Parachain → Relay Chain
dest = {
V3: {
parents: 1,
interior: 'Here',
},
},
};
};
beneficiary = {
V3: {
parents: 0,
interior: {
X1: {
AccountId32: {
network: null,
id: sourceApi.createType('AccountId32', address).toHex(),
},
},
},
},
};
// Native token from parachain's perspective (parent chain's token)
assets = {
V3: [
{
id: {
Concrete: {
parents: 1,
interior: 'Here',
},
},
fun: {
Fungible: amountInSmallestUnit.toString(),
},
},
],
};
}
const weightLimit = 'Unlimited';
// Create teleport transaction
const tx = api.tx.xcmPallet.limitedTeleportAssets(
dest,
beneficiary,
assets,
feeAssetId,
weightLimit
);
// Create teleport transaction using polkadotXcm pallet on parachains
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let tx: any;
if (direction === 'to-parachain') {
tx = sourceApi.tx.xcmPallet.limitedTeleportAssets(
dest,
beneficiary,
assets,
0,
weightLimit
);
} else {
// Parachains use polkadotXcm pallet
tx = sourceApi.tx.polkadotXcm.limitedTeleportAssets(
dest,
beneficiary,
assets,
0,
weightLimit
);
}
setTxStatus('pending');
const unsub = await tx.signAndSend(keypair, ({ status, dispatchError }) => {
if (status.isFinalized) {
if (dispatchError) {
let errorMessage = 'Teleport neserketî';
const unsub = await tx.signAndSend(
keypair,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
({ status, dispatchError }: any) => {
if (status.isFinalized) {
if (dispatchError) {
let errorMessage = 'Teleport neserketî';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}`;
if (dispatchError.isModule) {
const decoded = sourceApi.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}`;
}
setTxStatus('error');
hapticImpact('heavy');
showAlert(errorMessage);
} else {
setTxStatus('success');
hapticImpact('medium');
// Reset after success
setTimeout(() => {
setAmount('');
setTxStatus('idle');
onClose();
}, 2000);
}
setTxStatus('error');
hapticImpact('heavy');
showAlert(errorMessage);
} else {
setTxStatus('success');
hapticImpact('medium');
// Reset after success
setTimeout(() => {
setAmount('');
setTxStatus('idle');
onClose();
}, 2000);
setIsTransferring(false);
unsub();
}
setIsTransferring(false);
unsub();
}
});
);
} catch (error) {
console.error('Teleport error:', error);
setTxStatus('error');
@@ -264,13 +372,19 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
};
const setQuickAmount = (percent: number) => {
const balance = parseFloat(relayBalance);
const balance = parseFloat(getSourceBalance());
if (balance > 0) {
const quickAmount = ((balance * percent) / 100).toFixed(4);
setAmount(quickAmount);
}
};
const toggleDirection = () => {
setDirection((prev) => (prev === 'to-parachain' ? 'to-relay' : 'to-parachain'));
setAmount('');
hapticImpact('light');
};
if (!isOpen) return null;
return (
@@ -301,7 +415,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">Serketî!</h3>
<p className="text-muted-foreground">
{amount} HEZ bo {selectedChain.name} hate şandin
{amount} HEZ bo {getDestChainName()} hate şandin
</p>
</div>
) : txStatus === 'error' ? (
@@ -348,52 +462,70 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
</div>
</div>
{/* Direction Toggle */}
<div className="flex justify-center">
<button
onClick={toggleDirection}
disabled={isTransferring}
className="flex items-center gap-2 px-4 py-2 bg-muted/50 hover:bg-muted rounded-lg transition-colors"
>
<ArrowLeftRight className="w-4 h-4 text-yellow-500" />
<span className="text-sm">
{direction === 'to-parachain' ? 'Relay → Parachain' : 'Parachain → Relay'}
</span>
</button>
</div>
{/* Balance Display */}
<div className="bg-muted/50 rounded-xl 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" />
<span className="text-sm text-muted-foreground">Relay Chain</span>
<div
className={`w-2 h-2 rounded-full ${
direction === 'to-parachain'
? 'bg-green-500'
: targetChain === 'asset-hub'
? 'bg-blue-500'
: 'bg-purple-500'
}`}
/>
<span className="text-sm text-muted-foreground">{getSourceChainName()}</span>
</div>
<span className="font-mono">{relayBalance} HEZ</span>
<span className="font-mono">{getSourceBalance()} HEZ</span>
</div>
<div className="flex justify-center">
<ArrowDown className="w-5 h-5 text-yellow-500" />
{direction === 'to-parachain' ? (
<ArrowDown className="w-5 h-5 text-yellow-500" />
) : (
<ArrowUp 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 ${
targetChain === 'asset-hub' ? 'bg-blue-500' : 'bg-purple-500'
direction === 'to-parachain'
? targetChain === 'asset-hub'
? 'bg-blue-500'
: 'bg-purple-500'
: 'bg-green-500'
}`}
/>
<span className="text-sm text-muted-foreground">{selectedChain.name}</span>
<span className="text-sm text-muted-foreground">{getDestChainName()}</span>
</div>
<span className="font-mono">{getTargetBalance()} HEZ</span>
<span className="font-mono">{getDestBalance()} HEZ</span>
</div>
</div>
{/* Info Box */}
<div
className={`p-3 rounded-lg flex gap-2 ${
targetChain === 'asset-hub'
? 'bg-blue-500/10 border border-blue-500/30'
: 'bg-purple-500/10 border border-purple-500/30'
}`}
>
<Info
className={`w-5 h-5 flex-shrink-0 ${
targetChain === 'asset-hub' ? 'text-blue-400' : 'text-purple-400'
}`}
/>
<p
className={`text-sm ${
targetChain === 'asset-hub' ? 'text-blue-400' : 'text-purple-400'
}`}
>
{selectedChain.description} kêmî 0.1 HEZ pêşniyarkirin.
<div className="p-3 rounded-lg flex gap-2 bg-yellow-500/10 border border-yellow-500/30">
<Info className="w-5 h-5 flex-shrink-0 text-yellow-400" />
<p className="text-sm text-yellow-400">
{direction === 'to-parachain'
? `${selectedChain.description} kêmî 0.1 HEZ tê pêşniyarkirin.`
: 'HEZ ji parachainê vedigere Relay Chainê.'}
</p>
</div>
@@ -458,7 +590,7 @@ export function FundFeesModal({ isOpen, onClose }: Props) {
) : (
<>
<Fuel className="w-5 h-5" />
Bo {selectedChain.name} Bişîne
Bo {getDestChainName()} Bişîne
</>
)}
</button>
+3 -3
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.159",
"buildTime": "2026-02-07T14:59:50.270Z",
"buildNumber": 1770476390271
"version": "1.0.160",
"buildTime": "2026-02-07T18:31:26.019Z",
"buildNumber": 1770489086020
}