mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-20 22:11:04 +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,4 +1,4 @@
|
|||||||
name: Quality Gate
|
name: Quality Gate & Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,9 +7,17 @@ on:
|
|||||||
branches: [ main, develop ]
|
branches: [ main, develop ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ========================================
|
# ========================================
|
||||||
# WEB APP - BUILD, LINT & TEST
|
# WEB APP - LINT, TEST & BUILD
|
||||||
# ========================================
|
# ========================================
|
||||||
web:
|
web:
|
||||||
name: Web App
|
name: Web App
|
||||||
@@ -54,6 +62,12 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-dist
|
||||||
|
path: web/dist/
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# MOBILE APP - LINT & TEST
|
# MOBILE APP - LINT & TEST
|
||||||
# ========================================
|
# ========================================
|
||||||
@@ -91,13 +105,61 @@ jobs:
|
|||||||
run: npm run test
|
run: npm run test
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# SDK UI - BUILD & TEST (SKIPPED - uses root workspace)
|
# DEPLOY WEB APP TO VPS
|
||||||
# ========================================
|
# ========================================
|
||||||
# sdk-ui:
|
deploy:
|
||||||
# name: SDK UI
|
name: Deploy Web
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# # SDK UI requires the root yarn workspace, skipping for now
|
needs: [web, mobile]
|
||||||
# if: false
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Bump version
|
||||||
|
working-directory: ./web
|
||||||
|
run: |
|
||||||
|
npm version patch --no-git-tag-version
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
echo "NEW_VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
cd ..
|
||||||
|
git add web/package.json
|
||||||
|
git commit -m "chore(web): bump version to $VERSION [skip ci]" || echo "No version change"
|
||||||
|
git push || echo "Nothing to push"
|
||||||
|
|
||||||
|
- name: Download build artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-dist
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
- name: Deploy to VPS
|
||||||
|
uses: appleboy/scp-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
source: 'dist/*'
|
||||||
|
target: '/var/www/subdomains/app'
|
||||||
|
strip_components: 1
|
||||||
|
|
||||||
|
- name: Post-deploy notification
|
||||||
|
run: |
|
||||||
|
echo "✅ Deployed web app v${{ env.NEW_VERSION }} to app.pezkuwichain.io"
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# SECURITY CHECKS (INFORMATIVE)
|
# SECURITY CHECKS (INFORMATIVE)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -126,7 +126,7 @@ export async function getKycStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!api?.query?.identityKyc) {
|
if (!api?.query?.identityKyc) {
|
||||||
console.warn('Identity KYC pallet not available');
|
if (import.meta.env.DEV) console.log('Identity KYC pallet not available on this chain');
|
||||||
return 'NotStarted';
|
return 'NotStarted';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ export async function getUserTikis(
|
|||||||
): Promise<TikiInfo[]> {
|
): Promise<TikiInfo[]> {
|
||||||
try {
|
try {
|
||||||
if (!api?.query?.tiki?.userTikis) {
|
if (!api?.query?.tiki?.userTikis) {
|
||||||
console.warn('Tiki pallet not available');
|
if (import.meta.env.DEV) console.log('Tiki pallet not available on this chain');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ export async function isStakingScoreTracking(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
if (!api?.query?.stakingScore?.stakingStartBlock) {
|
if (!api?.query?.stakingScore?.stakingStartBlock) {
|
||||||
console.warn('Staking score pallet not available');
|
if (import.meta.env.DEV) console.log('Staking score pallet not available on this chain');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function checkCitizenStatus(
|
|||||||
try {
|
try {
|
||||||
// Check if Identity KYC pallet exists
|
// Check if Identity KYC pallet exists
|
||||||
if (!api.query?.identityKyc?.kycStatuses) {
|
if (!api.query?.identityKyc?.kycStatuses) {
|
||||||
console.warn('Identity KYC pallet not available');
|
if (import.meta.env.DEV) console.log('Identity KYC pallet not available on this chain');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export async function checkValidatorStatus(
|
|||||||
try {
|
try {
|
||||||
// Check if ValidatorPool pallet exists
|
// Check if ValidatorPool pallet exists
|
||||||
if (!api.query?.validatorPool?.poolMembers) {
|
if (!api.query?.validatorPool?.poolMembers) {
|
||||||
console.warn('ValidatorPool pallet not available');
|
if (import.meta.env.DEV) console.log('ValidatorPool pallet not available on this chain');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ export async function checkTikiRole(
|
|||||||
try {
|
try {
|
||||||
// Check if Tiki pallet exists
|
// Check if Tiki pallet exists
|
||||||
if (!api.query?.tiki?.userTikis) {
|
if (!api.query?.tiki?.userTikis) {
|
||||||
console.warn('Tiki pallet not available');
|
if (import.meta.env.DEV) console.log('Tiki pallet not available on this chain');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ export async function checkStakingScoreTracking(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!api.query?.stakingScore?.stakingStartBlock) {
|
if (!api.query?.stakingScore?.stakingStartBlock) {
|
||||||
console.warn('Staking score pallet not available');
|
if (import.meta.env.DEV) console.log('Staking score pallet not available on this chain');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-5
@@ -75,6 +75,13 @@ export async function initiateReferral(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the referral pallet is available on the chain
|
||||||
|
*/
|
||||||
|
function isReferralPalletAvailable(api: ApiPromise): boolean {
|
||||||
|
return !!(api.query.referral && api.query.referral.pendingReferrals);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the pending referral for a user (who invited them, if they haven't completed KYC)
|
* Get the pending referral for a user (who invited them, if they haven't completed KYC)
|
||||||
*
|
*
|
||||||
@@ -87,6 +94,12 @@ export async function getPendingReferral(
|
|||||||
address: string
|
address: string
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
|
// Check if referral pallet exists
|
||||||
|
if (!isReferralPalletAvailable(api)) {
|
||||||
|
if (import.meta.env.DEV) console.log('Referral pallet not available on this chain');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await api.query.referral.pendingReferrals(address);
|
const result = await api.query.referral.pendingReferrals(address);
|
||||||
|
|
||||||
if (result.isEmpty) {
|
if (result.isEmpty) {
|
||||||
@@ -95,7 +108,7 @@ export async function getPendingReferral(
|
|||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching pending referral:', error);
|
if (import.meta.env.DEV) console.error('Error fetching pending referral:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,10 +125,15 @@ export async function getReferralCount(
|
|||||||
address: string
|
address: string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
|
// Check if referral pallet exists
|
||||||
|
if (!isReferralPalletAvailable(api)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const count = await api.query.referral.referralCount(address);
|
const count = await api.query.referral.referralCount(address);
|
||||||
return count.toNumber();
|
return count.toNumber();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching referral count:', error);
|
if (import.meta.env.DEV) console.error('Error fetching referral count:', error);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +150,11 @@ export async function getReferralInfo(
|
|||||||
address: string
|
address: string
|
||||||
): Promise<ReferralInfo | null> {
|
): Promise<ReferralInfo | null> {
|
||||||
try {
|
try {
|
||||||
|
// Check if referral pallet exists
|
||||||
|
if (!isReferralPalletAvailable(api)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await api.query.referral.referrals(address);
|
const result = await api.query.referral.referrals(address);
|
||||||
|
|
||||||
if (result.isEmpty) {
|
if (result.isEmpty) {
|
||||||
@@ -144,7 +167,7 @@ export async function getReferralInfo(
|
|||||||
createdAt: parseInt(data.createdAt),
|
createdAt: parseInt(data.createdAt),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching referral info:', error);
|
if (import.meta.env.DEV) console.error('Error fetching referral info:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +205,16 @@ export async function getReferralStats(
|
|||||||
api: ApiPromise,
|
api: ApiPromise,
|
||||||
address: string
|
address: string
|
||||||
): Promise<ReferralStats> {
|
): Promise<ReferralStats> {
|
||||||
|
// Check if referral pallet exists first
|
||||||
|
if (!isReferralPalletAvailable(api)) {
|
||||||
|
return {
|
||||||
|
referralCount: 0,
|
||||||
|
referralScore: 0,
|
||||||
|
whoInvitedMe: null,
|
||||||
|
pendingReferral: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [referralCount, referralInfo, pendingReferral] = await Promise.all([
|
const [referralCount, referralInfo, pendingReferral] = await Promise.all([
|
||||||
getReferralCount(api, address),
|
getReferralCount(api, address),
|
||||||
@@ -198,7 +231,7 @@ export async function getReferralStats(
|
|||||||
pendingReferral,
|
pendingReferral,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching referral stats:', error);
|
if (import.meta.env.DEV) console.error('Error fetching referral stats:', error);
|
||||||
return {
|
return {
|
||||||
referralCount: 0,
|
referralCount: 0,
|
||||||
referralScore: 0,
|
referralScore: 0,
|
||||||
@@ -221,6 +254,11 @@ export async function getMyReferrals(
|
|||||||
referrerAddress: string
|
referrerAddress: string
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
|
// Check if referral pallet exists
|
||||||
|
if (!isReferralPalletAvailable(api)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const entries = await api.query.referral.referrals.entries();
|
const entries = await api.query.referral.referrals.entries();
|
||||||
|
|
||||||
const myReferrals = entries
|
const myReferrals = entries
|
||||||
@@ -237,7 +275,7 @@ export async function getMyReferrals(
|
|||||||
|
|
||||||
return myReferrals;
|
return myReferrals;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching my referrals:', error);
|
if (import.meta.env.DEV) console.error('Error fetching my referrals:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,6 +291,11 @@ export async function subscribeToReferralEvents(
|
|||||||
api: ApiPromise,
|
api: ApiPromise,
|
||||||
callback: (event: { type: 'initiated' | 'confirmed'; referrer: string; referred: string; count?: number }) => void
|
callback: (event: { type: 'initiated' | 'confirmed'; referrer: string; referred: string; count?: number }) => void
|
||||||
): Promise<() => void> {
|
): Promise<() => void> {
|
||||||
|
// Check if referral pallet exists - if not, return no-op unsubscribe
|
||||||
|
if (!isReferralPalletAvailable(api)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const unsub = await api.query.system.events((events: any[]) => {
|
const unsub = await api.query.system.events((events: any[]) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export async function getTrustScore(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
if (!api?.query?.trust) {
|
if (!api?.query?.trust) {
|
||||||
console.warn('Trust pallet not available');
|
// Trust pallet not available on this chain - this is expected
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ export async function getStakingScoreFromPallet(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
if (!api?.query?.stakingScore) {
|
if (!api?.query?.stakingScore) {
|
||||||
console.warn('Staking score pallet not available');
|
// Staking score pallet not available on this chain - this is expected
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -217,7 +217,7 @@ export const fetchUserTikis = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!api || !api.query.tiki) {
|
if (!api || !api.query.tiki) {
|
||||||
console.warn('Tiki pallet not available on this chain');
|
// Tiki pallet not available on this chain - this is expected
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@ export const fetchUserTikiNFTs = async (
|
|||||||
): Promise<TikiNFTDetails[]> => {
|
): Promise<TikiNFTDetails[]> => {
|
||||||
try {
|
try {
|
||||||
if (!api || !api.query.tiki) {
|
if (!api || !api.query.tiki) {
|
||||||
console.warn('Tiki pallet not available on this chain');
|
// Tiki pallet not available on this chain - this is expected
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -475,6 +475,224 @@ export async function testXCMTransfer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// XCM TELEPORT: RELAY CHAIN → ASSET HUB
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teleport HEZ from Relay Chain to Asset Hub
|
||||||
|
* This is needed to pay fees on Asset Hub for PEZ transfers
|
||||||
|
*
|
||||||
|
* @param relayApi - Polkadot.js API instance (connected to relay chain)
|
||||||
|
* @param amount - Amount in smallest unit (e.g., 100000000000 for 0.1 HEZ with 12 decimals)
|
||||||
|
* @param account - Account to sign and receive on Asset Hub
|
||||||
|
* @param assetHubParaId - Asset Hub parachain ID (default: 1000)
|
||||||
|
* @returns Transaction hash
|
||||||
|
*/
|
||||||
|
export async function teleportToAssetHub(
|
||||||
|
relayApi: ApiPromise,
|
||||||
|
amount: string | bigint,
|
||||||
|
account: InjectedAccountWithMeta,
|
||||||
|
assetHubParaId: number = 1000
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const injector = await (window as any).injectedWeb3[account.meta.source]?.enable?.('PezkuwiChain');
|
||||||
|
if (!injector) {
|
||||||
|
throw new Error('Failed to get injector from wallet extension');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = injector.signer;
|
||||||
|
|
||||||
|
// Destination: Asset Hub parachain
|
||||||
|
const dest = {
|
||||||
|
V3: {
|
||||||
|
parents: 0,
|
||||||
|
interior: {
|
||||||
|
X1: { Parachain: assetHubParaId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Beneficiary: Same account on Asset Hub
|
||||||
|
const beneficiary = {
|
||||||
|
V3: {
|
||||||
|
parents: 0,
|
||||||
|
interior: {
|
||||||
|
X1: {
|
||||||
|
AccountId32: {
|
||||||
|
network: null,
|
||||||
|
id: relayApi.createType('AccountId32', account.address).toHex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assets: Native token (HEZ)
|
||||||
|
const assets = {
|
||||||
|
V3: [{
|
||||||
|
id: {
|
||||||
|
Concrete: {
|
||||||
|
parents: 0,
|
||||||
|
interior: 'Here'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fun: {
|
||||||
|
Fungible: amount.toString()
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fee asset item (index 0 = first asset)
|
||||||
|
const feeAssetItem = 0;
|
||||||
|
|
||||||
|
// Weight limit: Unlimited
|
||||||
|
const weightLimit = 'Unlimited';
|
||||||
|
|
||||||
|
// Create teleport transaction
|
||||||
|
const tx = relayApi.tx.xcmPallet.limitedTeleportAssets(
|
||||||
|
dest,
|
||||||
|
beneficiary,
|
||||||
|
assets,
|
||||||
|
feeAssetItem,
|
||||||
|
weightLimit
|
||||||
|
);
|
||||||
|
|
||||||
|
let unsub: () => void;
|
||||||
|
|
||||||
|
await tx.signAndSend(account.address, { signer }, ({ status, events, dispatchError }) => {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = relayApi.registry.findMetaError(dispatchError.asModule);
|
||||||
|
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
|
||||||
|
} else {
|
||||||
|
reject(new Error(dispatchError.toString()));
|
||||||
|
}
|
||||||
|
if (unsub) unsub();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isInBlock) {
|
||||||
|
console.log(`✅ XCM Teleport included in block: ${status.asInBlock}`);
|
||||||
|
|
||||||
|
// Check for XCM events
|
||||||
|
const xcmSent = events.find(({ event }) =>
|
||||||
|
event.section === 'xcmPallet' && event.method === 'Sent'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (xcmSent) {
|
||||||
|
console.log('✅ XCM message sent successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(status.asInBlock.toString());
|
||||||
|
if (unsub) unsub();
|
||||||
|
}
|
||||||
|
}).then(unsubscribe => { unsub = unsubscribe; });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teleport HEZ from Asset Hub back to Relay Chain
|
||||||
|
*
|
||||||
|
* @param assetHubApi - Polkadot.js API instance (connected to Asset Hub)
|
||||||
|
* @param amount - Amount in smallest unit
|
||||||
|
* @param account - Account to sign and receive on relay chain
|
||||||
|
* @returns Transaction hash
|
||||||
|
*/
|
||||||
|
export async function teleportToRelayChain(
|
||||||
|
assetHubApi: ApiPromise,
|
||||||
|
amount: string | bigint,
|
||||||
|
account: InjectedAccountWithMeta
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const injector = await (window as any).injectedWeb3[account.meta.source]?.enable?.('PezkuwiChain');
|
||||||
|
if (!injector) {
|
||||||
|
throw new Error('Failed to get injector from wallet extension');
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = injector.signer;
|
||||||
|
|
||||||
|
// Destination: Relay chain (parent)
|
||||||
|
const dest = {
|
||||||
|
V3: {
|
||||||
|
parents: 1,
|
||||||
|
interior: 'Here'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Beneficiary: Same account on relay chain
|
||||||
|
const beneficiary = {
|
||||||
|
V3: {
|
||||||
|
parents: 0,
|
||||||
|
interior: {
|
||||||
|
X1: {
|
||||||
|
AccountId32: {
|
||||||
|
network: null,
|
||||||
|
id: assetHubApi.createType('AccountId32', account.address).toHex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assets: Native token
|
||||||
|
const assets = {
|
||||||
|
V3: [{
|
||||||
|
id: {
|
||||||
|
Concrete: {
|
||||||
|
parents: 1,
|
||||||
|
interior: 'Here'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fun: {
|
||||||
|
Fungible: amount.toString()
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
const feeAssetItem = 0;
|
||||||
|
const weightLimit = 'Unlimited';
|
||||||
|
|
||||||
|
const tx = assetHubApi.tx.polkadotXcm.limitedTeleportAssets(
|
||||||
|
dest,
|
||||||
|
beneficiary,
|
||||||
|
assets,
|
||||||
|
feeAssetItem,
|
||||||
|
weightLimit
|
||||||
|
);
|
||||||
|
|
||||||
|
let unsub: () => void;
|
||||||
|
|
||||||
|
await tx.signAndSend(account.address, { signer }, ({ status, dispatchError }) => {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||||
|
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
|
||||||
|
} else {
|
||||||
|
reject(new Error(dispatchError.toString()));
|
||||||
|
}
|
||||||
|
if (unsub) unsub();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.isInBlock) {
|
||||||
|
resolve(status.asInBlock.toString());
|
||||||
|
if (unsub) unsub();
|
||||||
|
}
|
||||||
|
}).then(unsubscribe => { unsub = unsubscribe; });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// UTILITY FUNCTIONS
|
// UTILITY FUNCTIONS
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vite_react_shadcn_ts",
|
"name": "vite_react_shadcn_ts",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "node generate-docs-structure.cjs",
|
"predev": "node generate-docs-structure.cjs",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||||
import { AddTokenModal } from './AddTokenModal';
|
import { AddTokenModal } from './AddTokenModal';
|
||||||
import { TransferModal } from './TransferModal';
|
import { TransferModal } from './TransferModal';
|
||||||
|
import { XCMTeleportModal } from './XCMTeleportModal';
|
||||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||||
|
|
||||||
interface TokenBalance {
|
interface TokenBalance {
|
||||||
@@ -18,7 +19,7 @@ interface TokenBalance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AccountBalance: React.FC = () => {
|
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<{
|
const [balance, setBalance] = useState<{
|
||||||
free: string;
|
free: string;
|
||||||
reserved: string;
|
reserved: string;
|
||||||
@@ -28,6 +29,9 @@ export const AccountBalance: React.FC = () => {
|
|||||||
reserved: '0',
|
reserved: '0',
|
||||||
total: '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 [pezBalance, setPezBalance] = useState<string>('0');
|
||||||
const [usdtBalance, setUsdtBalance] = useState<string>('0');
|
const [usdtBalance, setUsdtBalance] = useState<string>('0');
|
||||||
const [hezUsdPrice, setHezUsdPrice] = useState<number>(0);
|
const [hezUsdPrice, setHezUsdPrice] = useState<number>(0);
|
||||||
@@ -44,6 +48,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
const [otherTokens, setOtherTokens] = useState<TokenBalance[]>([]);
|
const [otherTokens, setOtherTokens] = useState<TokenBalance[]>([]);
|
||||||
const [isAddTokenModalOpen, setIsAddTokenModalOpen] = useState(false);
|
const [isAddTokenModalOpen, setIsAddTokenModalOpen] = useState(false);
|
||||||
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
|
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
|
||||||
|
const [isXCMTeleportModalOpen, setIsXCMTeleportModalOpen] = useState(false);
|
||||||
const [selectedTokenForTransfer, setSelectedTokenForTransfer] = useState<TokenBalance | null>(null);
|
const [selectedTokenForTransfer, setSelectedTokenForTransfer] = useState<TokenBalance | null>(null);
|
||||||
const [customTokenIds, setCustomTokenIds] = useState<number[]>(() => {
|
const [customTokenIds, setCustomTokenIds] = useState<number[]>(() => {
|
||||||
const stored = localStorage.getItem('customTokenIds');
|
const stored = localStorage.getItem('customTokenIds');
|
||||||
@@ -319,6 +324,36 @@ export const AccountBalance: React.FC = () => {
|
|||||||
total: totalTokens,
|
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
|
// Fetch PEZ balance (Asset ID: 1) from Asset Hub
|
||||||
try {
|
try {
|
||||||
if (assetHubApi && isAssetHubReady) {
|
if (assetHubApi && isAssetHubReady) {
|
||||||
@@ -538,59 +573,107 @@ export const AccountBalance: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<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">
|
<Card className="bg-gradient-to-br from-green-900/30 to-yellow-900/30 border-green-500/30">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img src="/tokens/HEZ.png" alt="HEZ" className="w-10 h-10 rounded-full" />
|
<img src="/tokens/HEZ.png" alt="HEZ" className="w-10 h-10 rounded-full" />
|
||||||
<CardTitle className="text-lg font-medium text-gray-300">
|
<div>
|
||||||
HEZ Balance
|
<CardTitle className="text-lg font-medium text-gray-300">
|
||||||
</CardTitle>
|
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>
|
</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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Total HEZ */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-4xl font-bold text-white mb-1">
|
<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>
|
<span className="text-2xl text-gray-400 ml-2">HEZ</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-gray-400">
|
||||||
{hezUsdPrice > 0
|
{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...'}
|
: 'Price loading...'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* Chain Balances */}
|
||||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
{/* Relay Chain (Main) */}
|
||||||
<TrendingUp className="w-4 h-4 text-green-400" />
|
<div className="bg-gray-800/50 rounded-lg p-3 border border-green-500/20">
|
||||||
<span className="text-xs text-gray-400">Transferable</span>
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-lg font-semibold text-white">
|
<div className="w-2 h-2 rounded-full bg-green-500"></div>
|
||||||
{balance.free} HEZ
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
{/* Asset Hub */}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="bg-gray-800/50 rounded-lg p-3 border border-blue-500/20">
|
||||||
<ArrowDownRight className="w-4 h-4 text-yellow-400" />
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-gray-400">Reserved</span>
|
<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>
|
||||||
<div className="text-lg font-semibold text-white">
|
</div>
|
||||||
{balance.reserved} HEZ
|
|
||||||
|
{/* 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -601,11 +684,26 @@ export const AccountBalance: React.FC = () => {
|
|||||||
{/* PEZ Balance Card */}
|
{/* PEZ Balance Card */}
|
||||||
<Card className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 border-blue-500/30">
|
<Card className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 border-blue-500/30">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<img src="/tokens/PEZ.png" alt="PEZ" className="w-10 h-10 rounded-full" />
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<CardTitle className="text-lg font-medium text-gray-300">
|
<img src="/tokens/PEZ.png" alt="PEZ" className="w-10 h-10 rounded-full flex-shrink-0" />
|
||||||
PEZ Token Balance
|
<CardTitle className="text-lg font-medium text-gray-300 whitespace-nowrap">
|
||||||
</CardTitle>
|
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>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -620,7 +718,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
: 'Price loading...'}
|
: 'Price loading...'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
Governance & Rewards Token
|
Governance & Rewards Token (on Asset Hub)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -848,6 +946,12 @@ export const AccountBalance: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
selectedAsset={selectedTokenForTransfer}
|
selectedAsset={selectedTokenForTransfer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* XCM Teleport Modal */}
|
||||||
|
<XCMTeleportModal
|
||||||
|
isOpen={isXCMTeleportModalOpen}
|
||||||
|
onClose={() => setIsXCMTeleportModalOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,25 +53,10 @@ const AppLayout: React.FC = () => {
|
|||||||
useWallet();
|
useWallet();
|
||||||
const [, _setIsAdmin] = useState(false);
|
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(() => {
|
React.useEffect(() => {
|
||||||
const checkAdminStatus = async () => {
|
_setIsAdmin(false); // Admin status managed by AuthContext
|
||||||
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();
|
|
||||||
}, [user]);
|
}, [user]);
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 text-white">
|
<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 }) => {
|
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 { toast } = useToast();
|
||||||
|
|
||||||
const [selectedToken, setSelectedToken] = useState<TokenType>('HEZ');
|
const [selectedToken, setSelectedToken] = useState<TokenType>('HEZ');
|
||||||
@@ -97,6 +97,17 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
|||||||
return;
|
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) {
|
if (!recipient || !amount) {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
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));
|
const amountInSmallestUnit = BigInt(parseFloat(amount) * Math.pow(10, currentToken.decimals));
|
||||||
|
|
||||||
let transfer;
|
let transfer;
|
||||||
|
let targetApi = api; // Default to main chain API
|
||||||
|
|
||||||
// Create appropriate transfer transaction based on token type
|
// 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)) {
|
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());
|
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 {
|
} 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());
|
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';
|
let errorMessage = 'Transaction failed';
|
||||||
|
|
||||||
if (dispatchError.isModule) {
|
if (dispatchError.isModule) {
|
||||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
const decoded = targetApi.registry.findMetaError(dispatchError.asModule);
|
||||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs}`;
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -134,23 +134,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECONDARY: Check Supabase admin_roles (if wallet not in whitelist)
|
// SECONDARY: Supabase admin_roles check disabled (table may not exist)
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
// Admin access is primarily wallet-based via the whitelist above
|
||||||
if (user) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('admin_roles')
|
|
||||||
.select('role')
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (!error && data && ['admin', 'super_admin'].includes(data.role)) {
|
if (import.meta.env.DEV) console.log('❌ Admin access denied (wallet not in whitelist)');
|
||||||
if (import.meta.env.DEV) console.log('✅ Admin access granted (Supabase-based)');
|
|
||||||
setIsAdmin(true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) console.log('❌ Admin access denied');
|
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
return false;
|
return false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
|||||||
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/pezkuwi';
|
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/pezkuwi';
|
||||||
import { isMobileApp, getNativeWalletAddress, getNativeAccountName } from '@/lib/mobile-bridge';
|
import { isMobileApp, getNativeWalletAddress, getNativeAccountName } from '@/lib/mobile-bridge';
|
||||||
|
|
||||||
// Asset Hub endpoint for PEZ token queries
|
// Parachain endpoints
|
||||||
const ASSET_HUB_ENDPOINT = 'wss://asset-hub-rpc.pezkuwichain.io';
|
const ASSET_HUB_ENDPOINT = 'wss://asset-hub-rpc.pezkuwichain.io';
|
||||||
|
const PEOPLE_CHAIN_ENDPOINT = 'wss://people-rpc.pezkuwichain.io';
|
||||||
|
|
||||||
interface PezkuwiContextType {
|
interface PezkuwiContextType {
|
||||||
api: ApiPromise | null;
|
api: ApiPromise | null;
|
||||||
assetHubApi: ApiPromise | null;
|
assetHubApi: ApiPromise | null;
|
||||||
|
peopleApi: ApiPromise | null;
|
||||||
isApiReady: boolean;
|
isApiReady: boolean;
|
||||||
isAssetHubReady: boolean;
|
isAssetHubReady: boolean;
|
||||||
|
isPeopleReady: boolean;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
accounts: InjectedAccountWithMeta[];
|
accounts: InjectedAccountWithMeta[];
|
||||||
selectedAccount: InjectedAccountWithMeta | null;
|
selectedAccount: InjectedAccountWithMeta | null;
|
||||||
@@ -36,8 +39,10 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [api, setApi] = useState<ApiPromise | null>(null);
|
const [api, setApi] = useState<ApiPromise | null>(null);
|
||||||
const [assetHubApi, setAssetHubApi] = useState<ApiPromise | null>(null);
|
const [assetHubApi, setAssetHubApi] = useState<ApiPromise | null>(null);
|
||||||
|
const [peopleApi, setPeopleApi] = useState<ApiPromise | null>(null);
|
||||||
const [isApiReady, setIsApiReady] = useState(false);
|
const [isApiReady, setIsApiReady] = useState(false);
|
||||||
const [isAssetHubReady, setIsAssetHubReady] = useState(false);
|
const [isAssetHubReady, setIsAssetHubReady] = useState(false);
|
||||||
|
const [isPeopleReady, setIsPeopleReady] = useState(false);
|
||||||
const [accounts, setAccounts] = useState<InjectedAccountWithMeta[]>([]);
|
const [accounts, setAccounts] = useState<InjectedAccountWithMeta[]>([]);
|
||||||
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
|
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -105,6 +110,17 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
|||||||
|
|
||||||
if (import.meta.env.DEV) console.log(`📡 Chain: ${chain}`);
|
if (import.meta.env.DEV) console.log(`📡 Chain: ${chain}`);
|
||||||
if (import.meta.env.DEV) console.log(`🖥️ Node: ${nodeName} v${nodeVersion}`);
|
if (import.meta.env.DEV) console.log(`🖥️ Node: ${nodeName} v${nodeVersion}`);
|
||||||
|
|
||||||
|
// Debug: Check Junction type definition
|
||||||
|
try {
|
||||||
|
const junctionType = apiInstance.createType('XcmV3Junction');
|
||||||
|
console.log('🔍 XCM Junction type keys:', (junctionType as any).defKeys || Object.keys(junctionType.toJSON() || {}));
|
||||||
|
// Expose api for console debugging
|
||||||
|
(window as any).__PEZKUWI_API__ = apiInstance;
|
||||||
|
console.log('💡 API exposed as window.__PEZKUWI_API__ for debugging');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('⚠️ Could not check Junction type:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch sudo key from blockchain
|
// Fetch sudo key from blockchain
|
||||||
@@ -168,8 +184,43 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize People Chain API for identity/citizenship
|
||||||
|
const initPeopleApi = async () => {
|
||||||
|
try {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('🔗 Connecting to People Chain:', PEOPLE_CHAIN_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new WsProvider(PEOPLE_CHAIN_ENDPOINT);
|
||||||
|
const peopleApiInstance = await ApiPromise.create({
|
||||||
|
provider,
|
||||||
|
signedExtensions: {
|
||||||
|
AuthorizeCall: {
|
||||||
|
extrinsic: {},
|
||||||
|
payload: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await peopleApiInstance.isReady;
|
||||||
|
|
||||||
|
setPeopleApi(peopleApiInstance);
|
||||||
|
setIsPeopleReady(true);
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('✅ Connected to People Chain for identity');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.error('❌ Failed to connect to People Chain:', err);
|
||||||
|
}
|
||||||
|
// Don't set error - Identity features just won't work
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
initApi();
|
initApi();
|
||||||
initAssetHubApi();
|
initAssetHubApi();
|
||||||
|
initPeopleApi();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (api) {
|
if (api) {
|
||||||
@@ -178,6 +229,9 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
|||||||
if (assetHubApi) {
|
if (assetHubApi) {
|
||||||
assetHubApi.disconnect();
|
assetHubApi.disconnect();
|
||||||
}
|
}
|
||||||
|
if (peopleApi) {
|
||||||
|
peopleApi.disconnect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [endpoint]);
|
}, [endpoint]);
|
||||||
@@ -357,8 +411,10 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
|||||||
const value: PezkuwiContextType = {
|
const value: PezkuwiContextType = {
|
||||||
api,
|
api,
|
||||||
assetHubApi,
|
assetHubApi,
|
||||||
|
peopleApi,
|
||||||
isApiReady,
|
isApiReady,
|
||||||
isAssetHubReady,
|
isAssetHubReady,
|
||||||
|
isPeopleReady,
|
||||||
isConnected: isApiReady, // Alias for backward compatibility
|
isConnected: isApiReady, // Alias for backward compatibility
|
||||||
accounts,
|
accounts,
|
||||||
selectedAccount,
|
selectedAccount,
|
||||||
|
|||||||
@@ -53,15 +53,10 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||||||
const connectionAttempts = useRef(0);
|
const connectionAttempts = useRef(0);
|
||||||
|
|
||||||
const connect = useCallback((endpointIndex: number = 0) => {
|
const connect = useCallback((endpointIndex: number = 0) => {
|
||||||
// If we've tried all endpoints, show error once and stop
|
// If we've tried all endpoints, stop silently (WebSocket is optional)
|
||||||
if (endpointIndex >= ENDPOINTS.length) {
|
if (endpointIndex >= ENDPOINTS.length) {
|
||||||
if (!hasShownFinalError.current) {
|
if (!hasShownFinalError.current) {
|
||||||
if (import.meta.env.DEV) console.error('❌ All WebSocket endpoints failed');
|
// WebSocket service is optional - fail silently
|
||||||
toast({
|
|
||||||
title: "Real-time Connection Unavailable",
|
|
||||||
description: "Could not connect to WebSocket server. Live updates will be disabled.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
hasShownFinalError.current = true;
|
hasShownFinalError.current = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const WalletDashboard: React.FC = () => {
|
|||||||
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>([]);
|
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>([]);
|
||||||
const [isLoadingRecent, setIsLoadingRecent] = useState(false);
|
const [isLoadingRecent, setIsLoadingRecent] = useState(false);
|
||||||
|
|
||||||
// Fetch recent transactions
|
// Fetch recent transactions (limited to last 10 blocks for performance)
|
||||||
const fetchRecentTransactions = async () => {
|
const fetchRecentTransactions = async () => {
|
||||||
if (!api || !isApiReady || !selectedAccount) return;
|
if (!api || !isApiReady || !selectedAccount) return;
|
||||||
|
|
||||||
@@ -41,7 +41,8 @@ const WalletDashboard: React.FC = () => {
|
|||||||
const currentBlockNumber = currentBlock.block.header.number.toNumber();
|
const currentBlockNumber = currentBlock.block.header.number.toNumber();
|
||||||
|
|
||||||
const txList: Transaction[] = [];
|
const txList: Transaction[] = [];
|
||||||
const blocksToCheck = Math.min(100, currentBlockNumber);
|
// Only check last 10 blocks for performance (proper indexer needed for full history)
|
||||||
|
const blocksToCheck = Math.min(10, currentBlockNumber);
|
||||||
|
|
||||||
for (let i = 0; i < blocksToCheck && txList.length < 5; i++) {
|
for (let i = 0; i < blocksToCheck && txList.length < 5; i++) {
|
||||||
const blockNumber = currentBlockNumber - i;
|
const blockNumber = currentBlockNumber - i;
|
||||||
@@ -303,9 +304,9 @@ const WalletDashboard: React.FC = () => {
|
|||||||
) : recentTransactions.length === 0 ? (
|
) : recentTransactions.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<History className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
<History className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||||
<p className="text-gray-500">No recent transactions</p>
|
<p className="text-gray-500">No recent transactions found</p>
|
||||||
<p className="text-gray-600 text-sm mt-1">
|
<p className="text-gray-600 text-sm mt-1">
|
||||||
Your transaction history will appear here
|
Recent activity from last 10 blocks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user