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:
2026-02-04 11:35:25 +03:00
parent 9d57473000
commit 02094a3635
18 changed files with 1049 additions and 113 deletions
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -126,7 +126,7 @@ export async function getKycStatus(
}
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';
}
@@ -195,7 +195,7 @@ export async function getUserTikis(
): Promise<TikiInfo[]> {
try {
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 [];
}
@@ -282,7 +282,7 @@ export async function isStakingScoreTracking(
): Promise<boolean> {
try {
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;
}
+4 -4
View File
@@ -24,7 +24,7 @@ export async function checkCitizenStatus(
try {
// Check if Identity KYC pallet exists
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;
}
@@ -61,7 +61,7 @@ export async function checkValidatorStatus(
try {
// Check if ValidatorPool pallet exists
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;
}
@@ -142,7 +142,7 @@ export async function checkTikiRole(
try {
// Check if Tiki pallet exists
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;
}
@@ -285,7 +285,7 @@ export async function checkStakingScoreTracking(
try {
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;
}
+48 -5
View File
@@ -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)
*
@@ -87,6 +94,12 @@ export async function getPendingReferral(
address: string
): Promise<string | null> {
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);
if (result.isEmpty) {
@@ -95,7 +108,7 @@ export async function getPendingReferral(
return result.toString();
} catch (error) {
console.error('Error fetching pending referral:', error);
if (import.meta.env.DEV) console.error('Error fetching pending referral:', error);
return null;
}
}
@@ -112,10 +125,15 @@ export async function getReferralCount(
address: string
): Promise<number> {
try {
// Check if referral pallet exists
if (!isReferralPalletAvailable(api)) {
return 0;
}
const count = await api.query.referral.referralCount(address);
return count.toNumber();
} catch (error) {
console.error('Error fetching referral count:', error);
if (import.meta.env.DEV) console.error('Error fetching referral count:', error);
return 0;
}
}
@@ -132,6 +150,11 @@ export async function getReferralInfo(
address: string
): Promise<ReferralInfo | null> {
try {
// Check if referral pallet exists
if (!isReferralPalletAvailable(api)) {
return null;
}
const result = await api.query.referral.referrals(address);
if (result.isEmpty) {
@@ -144,7 +167,7 @@ export async function getReferralInfo(
createdAt: parseInt(data.createdAt),
};
} catch (error) {
console.error('Error fetching referral info:', error);
if (import.meta.env.DEV) console.error('Error fetching referral info:', error);
return null;
}
}
@@ -182,6 +205,16 @@ export async function getReferralStats(
api: ApiPromise,
address: string
): Promise<ReferralStats> {
// Check if referral pallet exists first
if (!isReferralPalletAvailable(api)) {
return {
referralCount: 0,
referralScore: 0,
whoInvitedMe: null,
pendingReferral: null,
};
}
try {
const [referralCount, referralInfo, pendingReferral] = await Promise.all([
getReferralCount(api, address),
@@ -198,7 +231,7 @@ export async function getReferralStats(
pendingReferral,
};
} catch (error) {
console.error('Error fetching referral stats:', error);
if (import.meta.env.DEV) console.error('Error fetching referral stats:', error);
return {
referralCount: 0,
referralScore: 0,
@@ -221,6 +254,11 @@ export async function getMyReferrals(
referrerAddress: string
): Promise<string[]> {
try {
// Check if referral pallet exists
if (!isReferralPalletAvailable(api)) {
return [];
}
const entries = await api.query.referral.referrals.entries();
const myReferrals = entries
@@ -237,7 +275,7 @@ export async function getMyReferrals(
return myReferrals;
} catch (error) {
console.error('Error fetching my referrals:', error);
if (import.meta.env.DEV) console.error('Error fetching my referrals:', error);
return [];
}
}
@@ -253,6 +291,11 @@ export async function subscribeToReferralEvents(
api: ApiPromise,
callback: (event: { type: 'initiated' | 'confirmed'; referrer: string; referred: string; count?: number }) => 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
const unsub = await api.query.system.events((events: any[]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+2 -2
View File
@@ -40,7 +40,7 @@ export async function getTrustScore(
): Promise<number> {
try {
if (!api?.query?.trust) {
console.warn('Trust pallet not available');
// Trust pallet not available on this chain - this is expected
return 0;
}
@@ -200,7 +200,7 @@ export async function getStakingScoreFromPallet(
): Promise<number> {
try {
if (!api?.query?.stakingScore) {
console.warn('Staking score pallet not available');
// Staking score pallet not available on this chain - this is expected
return 0;
}
+2 -2
View File
@@ -217,7 +217,7 @@ export const fetchUserTikis = async (
}
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 [];
}
@@ -437,7 +437,7 @@ export const fetchUserTikiNFTs = async (
): Promise<TikiNFTDetails[]> => {
try {
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 [];
}
+218
View File
@@ -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
// ========================================