/** * XCM Bridge Service * * Handles Asset Hub USDT → wUSDT bridge configuration * User-friendly abstraction over complex XCM operations * * ALFA TESTNET MODE: Mock XCM for standalone chain testing * BETA+ MODE: Real XCM with Rococo/Westend Asset Hub */ import { ApiPromise, WsProvider } from '@pezkuwi/api'; import type { Signer } from '@pezkuwi/api/types'; // Detect mock mode (alfa testnet) const IS_MOCK_MODE = typeof process !== 'undefined' ? process.env.VITE_MOCK_XCM === 'true' : typeof import.meta !== 'undefined' ? import.meta.env?.VITE_MOCK_XCM === 'true' : false; // Mock XCM state management (localStorage) const MOCK_XCM_STORAGE_KEY = 'pezkuwi_mock_xcm_configured'; function getMockXcmConfigured(): boolean { if (typeof window === 'undefined') return false; return localStorage.getItem(MOCK_XCM_STORAGE_KEY) === 'true'; } function setMockXcmConfigured(configured: boolean): void { if (typeof window === 'undefined') return; localStorage.setItem(MOCK_XCM_STORAGE_KEY, String(configured)); } // Westend Asset Hub endpoint (production) export const ASSET_HUB_ENDPOINT = 'wss://westend-asset-hub-rpc.polkadot.io'; // Known Asset IDs export const ASSET_HUB_USDT_ID = 1984; // USDT on Asset Hub export const WUSDT_ASSET_ID = 1000; // wUSDT on PezkuwiChain (was 2, now 1000) export const ASSET_HUB_PARACHAIN_ID = 1000; /** * Bridge status information */ export interface BridgeStatus { isConfigured: boolean; assetHubLocation: string | null; usdtMapping: number | null; assetHubConnected: boolean; wusdtExists: boolean; } /** * Asset Hub USDT metadata */ export interface AssetHubUsdtInfo { id: number; name: string; symbol: string; decimals: number; supply: string; } /** * Connect to Asset Hub */ export async function connectToAssetHub(): Promise { if (IS_MOCK_MODE) { console.log('[MOCK XCM] Simulating Asset Hub connection for alfa testnet'); // Return null to signal mock mode - calling code will handle gracefully return null as any; } try { const provider = new WsProvider(ASSET_HUB_ENDPOINT); const api = await ApiPromise.create({ provider }); await api.isReady; return api; } catch (error) { console.error('Failed to connect to Asset Hub:', error); throw new Error(`Asset Hub connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Fetch Asset Hub USDT metadata */ export async function fetchAssetHubUsdtInfo( assetHubApi?: ApiPromise ): Promise { if (IS_MOCK_MODE) { console.log('[MOCK XCM] Returning simulated Asset Hub USDT info'); return { id: ASSET_HUB_USDT_ID, name: 'Tether USD', symbol: 'USDT', decimals: 6, supply: '1000000000000000', // 1 billion USDT (simulated) }; } let api = assetHubApi; let shouldDisconnect = false; try { // Connect if not provided if (!api) { api = await connectToAssetHub(); shouldDisconnect = true; } // Fetch USDT metadata from Asset Hub const metadata = await api.query.assets.metadata(ASSET_HUB_USDT_ID); const metadataJson = metadata.toJSON() as any; // Fetch total supply const asset = await api.query.assets.asset(ASSET_HUB_USDT_ID); const assetJson = asset.toJSON() as any; return { id: ASSET_HUB_USDT_ID, name: metadataJson?.name || 'Unknown', symbol: metadataJson?.symbol || 'USDT', decimals: metadataJson?.decimals || 6, supply: assetJson?.supply?.toString() || '0', }; } catch (error) { console.error('Failed to fetch Asset Hub USDT info:', error); throw new Error(`Failed to fetch USDT info: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { if (shouldDisconnect && api) { await api.disconnect(); } } } /** * Check current XCM bridge configuration status */ export async function checkBridgeStatus( api: ApiPromise ): Promise { try { // Check if wUSDT asset exists const wusdtAsset = await api.query.assets.asset(WUSDT_ASSET_ID); const wusdtExists = wusdtAsset.isSome; // Mock mode: Simulate successful bridge setup if (IS_MOCK_MODE) { const isConfigured = getMockXcmConfigured(); console.log('[MOCK XCM] Returning simulated bridge status for alfa testnet (configured:', isConfigured, ')'); return { isConfigured, assetHubLocation: isConfigured ? `ParaId(${ASSET_HUB_PARACHAIN_ID})` : null, usdtMapping: isConfigured ? WUSDT_ASSET_ID : null, assetHubConnected: true, // Simulated connection success wusdtExists, }; } // Try to connect to Asset Hub let assetHubConnected = false; try { const assetHubApi = await connectToAssetHub(); assetHubConnected = assetHubApi.isConnected; await assetHubApi.disconnect(); } catch { assetHubConnected = false; } // TODO: Check XCM configuration // This requires checking the runtime configuration // For now, we'll return a basic status const isConfigured = false; // Will be updated when XCM pallet is available return { isConfigured, assetHubLocation: isConfigured ? `ParaId(${ASSET_HUB_PARACHAIN_ID})` : null, usdtMapping: isConfigured ? WUSDT_ASSET_ID : null, assetHubConnected, wusdtExists, }; } catch (error) { console.error('Failed to check bridge status:', error); return { isConfigured: false, assetHubLocation: null, usdtMapping: null, assetHubConnected: false, wusdtExists: false, }; } } /** * Configure XCM bridge (requires sudo access) * * This sets up the ForeignAssetTransactor to map Asset Hub USDT → wUSDT */ export async function configureXcmBridge( api: ApiPromise, signer: Signer, account: string, onStatusUpdate?: (status: string) => void ): Promise { if (!api.tx.sudo) { throw new Error('Sudo pallet not available'); } // Mock mode: Simulate successful configuration if (IS_MOCK_MODE) { console.log('[MOCK XCM] Simulating XCM bridge configuration for alfa testnet'); onStatusUpdate?.('Preparing XCM configuration...'); await new Promise(resolve => setTimeout(resolve, 500)); onStatusUpdate?.('Simulating sudo transaction...'); await new Promise(resolve => setTimeout(resolve, 1000)); onStatusUpdate?.('Mock XCM bridge configured successfully!'); // Store mock configuration state setMockXcmConfigured(true); // Return mock transaction hash return '0x' + '0'.repeat(64); } try { onStatusUpdate?.('Preparing XCM configuration...'); // Create Asset Hub location const assetHubLocation = { parents: 1, interior: { X2: [ { Parachain: ASSET_HUB_PARACHAIN_ID }, { GeneralIndex: ASSET_HUB_USDT_ID } ] } }; // Note: This is a placeholder for the actual XCM configuration // The actual implementation depends on the runtime's XCM configuration pallet // For now, we'll document the expected transaction structure console.log('XCM Configuration (Placeholder):', { assetHubLocation, wusdtAssetId: WUSDT_ASSET_ID, note: 'Actual implementation requires XCM config pallet in runtime' }); onStatusUpdate?.('Waiting for user signature...'); // TODO: Implement actual XCM configuration when pallet is available // const configTx = api.tx.sudo.sudo( // api.tx.xcmConfig.configureForeignAsset(assetHubLocation, WUSDT_ASSET_ID) // ); // For now, return a placeholder return 'XCM configuration transaction placeholder - requires runtime XCM config pallet'; } catch (error) { console.error('Failed to configure XCM bridge:', error); throw new Error(`XCM configuration failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Create wUSDT/HEZ liquidity pool */ export async function createWUsdtHezPool( api: ApiPromise, signer: Signer, account: string, wusdtAmount: string, hezAmount: string, onStatusUpdate?: (status: string) => void ): Promise { try { onStatusUpdate?.('Creating wUSDT/HEZ pool...'); // Create pool transaction const poolTx = api.tx.assetConversion.createPool( { Assets: WUSDT_ASSET_ID }, // wUSDT 'Native' // Native HEZ ); onStatusUpdate?.('Adding initial liquidity...'); // Add liquidity transaction const liquidityTx = api.tx.assetConversion.addLiquidity( { Assets: WUSDT_ASSET_ID }, 'Native', wusdtAmount, hezAmount, '0', // min_mint_amount account ); onStatusUpdate?.('Batching transactions...'); // Batch both transactions const batchTx = api.tx.utility.batchAll([poolTx, liquidityTx]); onStatusUpdate?.('Waiting for signature...'); // Sign and send return new Promise((resolve, reject) => { batchTx.signAndSend( account, { signer }, ({ status, dispatchError, events }) => { if (status.isInBlock) { if (dispatchError) { if (dispatchError.isModule) { const decoded = api.registry.findMetaError(dispatchError.asModule); reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`)); } else { reject(new Error(dispatchError.toString())); } } else { onStatusUpdate?.('Pool created successfully!'); resolve(status.asInBlock.toHex()); } } } ); }); } catch (error) { console.error('Failed to create wUSDT/HEZ pool:', error); throw new Error(`Pool creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Verify wUSDT asset exists on chain */ export async function verifyWUsdtAsset(api: ApiPromise): Promise { try { const asset = await api.query.assets.asset(WUSDT_ASSET_ID); return asset.isSome; } catch (error) { console.error('Failed to verify wUSDT asset:', error); return false; } } /** * Get wUSDT asset details */ export async function getWUsdtAssetDetails(api: ApiPromise) { try { const [asset, metadata] = await Promise.all([ api.query.assets.asset(WUSDT_ASSET_ID), api.query.assets.metadata(WUSDT_ASSET_ID), ]); if (!asset.isSome) { return null; } const assetData = asset.unwrap().toJSON() as any; const metadataData = metadata.toJSON() as any; return { supply: assetData.supply?.toString() || '0', owner: assetData.owner, issuer: assetData.issuer, admin: assetData.admin, freezer: assetData.freezer, minBalance: assetData.minBalance?.toString() || '0', name: metadataData.name || 'wUSDT', symbol: metadataData.symbol || 'wUSDT', decimals: metadataData.decimals || 6, }; } catch (error) { console.error('Failed to get wUSDT asset details:', error); return null; } } /** * Format XCM location for display */ export function formatXcmLocation(location: any): string { if (typeof location === 'string') return location; try { if (location.parents !== undefined) { const junctions = location.interior?.X2 || location.interior?.X1 || []; return `RelayChain → ${junctions.map((j: any) => { if (j.Parachain) return `Para(${j.Parachain})`; if (j.GeneralIndex) return `Asset(${j.GeneralIndex})`; return JSON.stringify(j); }).join(' → ')}`; } return JSON.stringify(location); } catch { return 'Invalid location'; } }