mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-22 03:07:55 +00:00
feat: add bidirectional HEZ teleport (Relay ↔ Parachain)
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 tê 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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user