mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-10-25 12:55:02 +00:00
chore(dex): remove the XCM configuration wizard (#59)
The founder-only wizard registered PezkuwiChain as a teyrchain on Westend, Rococo or Polkadot (Rococo is shut down) and does nothing in the current architecture. XCMBridgeSetupModal was not mounted anywhere. Remove both, their xcm-wizard and xcm-bridge libraries, the DEX admin card, their translations in all six locales, and the typecheck baseline entries for the deleted files.
This commit is contained in:
1 parent
b778493eaa
commit
ac730481d9
12 files changed
-3041
No files matched your search
@@ -1,403 +0,0 @@
|
||||
/**
|
||||
* 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<ApiPromise> {
|
||||
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<AssetHubUsdtInfo> {
|
||||
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<BridgeStatus> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<boolean> {
|
||||
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() as any).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';
|
||||
}
|
||||
}
|
||||
@@ -1,724 +0,0 @@
|
||||
/**
|
||||
* XCM Configuration Wizard Backend Functions
|
||||
*
|
||||
* Handles parachain registration, HRMP channels, foreign asset registration,
|
||||
* and XCM transfer testing for PezkuwiChain.
|
||||
*/
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
||||
|
||||
// ========================================
|
||||
// TYPES
|
||||
// ========================================
|
||||
|
||||
export type RelayChain = 'westend' | 'rococo' | 'polkadot';
|
||||
|
||||
export interface ChainArtifacts {
|
||||
genesisPath: string;
|
||||
genesisSize: number;
|
||||
wasmPath: string;
|
||||
wasmSize: number;
|
||||
}
|
||||
|
||||
export interface HRMPChannel {
|
||||
sender: number;
|
||||
receiver: number;
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
export interface AssetMetadata {
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
minBalance: string;
|
||||
}
|
||||
|
||||
export interface ForeignAsset {
|
||||
symbol: string;
|
||||
location: {
|
||||
parents: number;
|
||||
interior: any; // XCM Location interior
|
||||
};
|
||||
metadata: AssetMetadata;
|
||||
}
|
||||
|
||||
export interface RegisteredAsset {
|
||||
assetId: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface XCMTestResult {
|
||||
txHash: string;
|
||||
success: boolean;
|
||||
balance: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STEP 1: RESERVE PARAID
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Reserve a ParaId on the relay chain
|
||||
*
|
||||
* @param api - Polkadot.js API instance (connected to relay chain)
|
||||
* @param relayChain - Target relay chain (westend/rococo/polkadot)
|
||||
* @param account - Account to sign the transaction
|
||||
* @returns Reserved ParaId number
|
||||
*/
|
||||
export async function reserveParaId(
|
||||
api: ApiPromise,
|
||||
relayChain: RelayChain,
|
||||
account: InjectedAccountWithMeta
|
||||
): Promise<number> {
|
||||
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;
|
||||
|
||||
// Call registrar.reserve() on relay chain
|
||||
const tx = api.tx.registrar.reserve();
|
||||
|
||||
let unsub: () => void;
|
||||
|
||||
await tx.signAndSend(account.address, { signer }, ({ status, events, dispatchError }) => {
|
||||
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()));
|
||||
}
|
||||
if (unsub) unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.isInBlock) {
|
||||
// Extract ParaId from events
|
||||
const reservedEvent = events.find(({ event }) =>
|
||||
api.events.registrar.Reserved.is(event)
|
||||
);
|
||||
|
||||
if (reservedEvent) {
|
||||
const paraId = reservedEvent.event.data[0].toNumber();
|
||||
resolve(paraId);
|
||||
if (unsub) unsub();
|
||||
} else {
|
||||
reject(new Error('ParaId reservation failed: No Reserved event found'));
|
||||
if (unsub) unsub();
|
||||
}
|
||||
}
|
||||
}).then(unsubscribe => { unsub = unsubscribe; });
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STEP 2: GENERATE CHAIN ARTIFACTS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Generate genesis state and runtime WASM for parachain
|
||||
*
|
||||
* Note: This is a simplified version. In production, you'd call
|
||||
* your blockchain node CLI to generate these artifacts.
|
||||
*
|
||||
* @param chainName - Name of the parachain
|
||||
* @returns Paths to generated artifacts
|
||||
*/
|
||||
export async function generateChainArtifacts(
|
||||
chainName: string
|
||||
): Promise<ChainArtifacts> {
|
||||
// In a real implementation, this would:
|
||||
// 1. Call: ./target/release/pezkuwi export-genesis-state --chain=<chain-spec> > genesis-head.hex
|
||||
// 2. Call: ./target/release/pezkuwi export-genesis-wasm --chain=<chain-spec> > runtime.wasm
|
||||
// 3. Return the file paths and sizes
|
||||
|
||||
// For now, we'll return placeholder paths
|
||||
// The actual implementation should use Node.js child_process or a backend API
|
||||
|
||||
return {
|
||||
genesisPath: `/tmp/pezkuwi-${chainName}-genesis.hex`,
|
||||
genesisSize: 0, // Would be actual file size
|
||||
wasmPath: `/tmp/pezkuwi-${chainName}-runtime.wasm`,
|
||||
wasmSize: 0, // Would be actual file size
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STEP 3: REGISTER PARACHAIN
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Register parachain on relay chain with genesis and WASM
|
||||
*
|
||||
* @param api - Polkadot.js API instance (relay chain)
|
||||
* @param paraId - Reserved ParaId
|
||||
* @param genesisFile - Genesis state file
|
||||
* @param wasmFile - Runtime WASM file
|
||||
* @param account - Account to sign transaction
|
||||
* @returns Transaction hash
|
||||
*/
|
||||
export async function registerParachain(
|
||||
api: ApiPromise,
|
||||
paraId: number,
|
||||
genesisFile: File,
|
||||
wasmFile: File,
|
||||
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;
|
||||
|
||||
// Read files as hex strings
|
||||
const genesisHex = await readFileAsHex(genesisFile);
|
||||
const wasmHex = await readFileAsHex(wasmFile);
|
||||
|
||||
// Call registrar.register() with paraId, genesis, and wasm
|
||||
const tx = api.tx.registrar.register(paraId, genesisHex, wasmHex);
|
||||
|
||||
let unsub: () => void;
|
||||
|
||||
await tx.signAndSend(account.address, { signer }, ({ status, dispatchError }) => {
|
||||
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()));
|
||||
}
|
||||
if (unsub) unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.isInBlock) {
|
||||
resolve(status.asInBlock.toString());
|
||||
if (unsub) unsub();
|
||||
}
|
||||
}).then(unsubscribe => { unsub = unsubscribe; });
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Read File as hex string
|
||||
*/
|
||||
async function readFileAsHex(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const arrayBuffer = reader.result as ArrayBuffer;
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const hex = '0x' + Array.from(uint8Array)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
resolve(hex);
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file'));
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STEP 4: OPEN HRMP CHANNELS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Open bidirectional HRMP channels with target parachains
|
||||
*
|
||||
* @param api - Polkadot.js API instance (relay chain)
|
||||
* @param paraId - Our ParaId
|
||||
* @param targetParas - List of target ParaIds (e.g., [1000] for Asset Hub)
|
||||
* @param account - Account to sign transactions
|
||||
* @returns Array of opened channels
|
||||
*/
|
||||
export async function openHRMPChannels(
|
||||
api: ApiPromise,
|
||||
paraId: number,
|
||||
targetParas: number[],
|
||||
account: InjectedAccountWithMeta
|
||||
): Promise<HRMPChannel[]> {
|
||||
const channels: HRMPChannel[] = [];
|
||||
|
||||
for (const targetParaId of targetParas) {
|
||||
// Open channel: paraId → targetParaId
|
||||
const outgoingChannel = await openHRMPChannel(api, paraId, targetParaId, account);
|
||||
channels.push(outgoingChannel);
|
||||
|
||||
// Open channel: targetParaId → paraId (requires governance or target's approval)
|
||||
// Note: In practice, this requires the target parachain to initiate
|
||||
// For Asset Hub and system chains, this is usually done via governance
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a single HRMP channel
|
||||
*/
|
||||
async function openHRMPChannel(
|
||||
api: ApiPromise,
|
||||
sender: number,
|
||||
receiver: number,
|
||||
account: InjectedAccountWithMeta
|
||||
): Promise<HRMPChannel> {
|
||||
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;
|
||||
|
||||
// Call hrmp.hrmpInitOpenChannel(recipient, proposedMaxCapacity, proposedMaxMessageSize)
|
||||
const maxCapacity = 1000;
|
||||
const maxMessageSize = 102400; // 100 KB
|
||||
|
||||
const tx = api.tx.hrmp.hrmpInitOpenChannel(receiver, maxCapacity, maxMessageSize);
|
||||
|
||||
let unsub: () => void;
|
||||
|
||||
await tx.signAndSend(account.address, { signer }, ({ status, events, dispatchError }) => {
|
||||
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()));
|
||||
}
|
||||
if (unsub) unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.isInBlock) {
|
||||
const channelId = status.asInBlock.toString();
|
||||
resolve({ sender, receiver, channelId });
|
||||
if (unsub) unsub();
|
||||
}
|
||||
}).then(unsubscribe => { unsub = unsubscribe; });
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STEP 5: REGISTER FOREIGN ASSETS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Register foreign assets from other chains (via XCM)
|
||||
*
|
||||
* @param api - Polkadot.js API instance (our parachain)
|
||||
* @param assets - List of foreign assets to register
|
||||
* @param account - Account to sign transactions
|
||||
* @returns List of registered assets with Asset IDs
|
||||
*/
|
||||
export async function registerForeignAssets(
|
||||
api: ApiPromise,
|
||||
assets: ForeignAsset[],
|
||||
account: InjectedAccountWithMeta
|
||||
): Promise<RegisteredAsset[]> {
|
||||
const registered: RegisteredAsset[] = [];
|
||||
|
||||
for (const asset of assets) {
|
||||
const registeredAsset = await registerSingleAsset(api, asset, account);
|
||||
registered.push(registeredAsset);
|
||||
}
|
||||
|
||||
return registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single foreign asset
|
||||
*/
|
||||
async function registerSingleAsset(
|
||||
api: ApiPromise,
|
||||
asset: ForeignAsset,
|
||||
account: InjectedAccountWithMeta
|
||||
): Promise<RegisteredAsset> {
|
||||
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;
|
||||
|
||||
// Get next available asset ID
|
||||
const nextAssetId = await getNextAssetId(api);
|
||||
|
||||
// Create asset with metadata
|
||||
// Note: Adjust based on your pallet configuration
|
||||
const createTx = api.tx.assets.create(
|
||||
nextAssetId,
|
||||
account.address, // Admin
|
||||
asset.metadata.minBalance
|
||||
);
|
||||
|
||||
const setMetadataTx = api.tx.assets.setMetadata(
|
||||
nextAssetId,
|
||||
asset.metadata.name,
|
||||
asset.metadata.symbol,
|
||||
asset.metadata.decimals
|
||||
);
|
||||
|
||||
// Batch both transactions
|
||||
const tx = api.tx.utility.batchAll([createTx, setMetadataTx]);
|
||||
|
||||
let unsub: () => void;
|
||||
|
||||
await tx.signAndSend(account.address, { signer }, ({ status, dispatchError }) => {
|
||||
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()));
|
||||
}
|
||||
if (unsub) unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.isInBlock) {
|
||||
resolve({
|
||||
assetId: nextAssetId,
|
||||
symbol: asset.metadata.symbol,
|
||||
});
|
||||
if (unsub) unsub();
|
||||
}
|
||||
}).then(unsubscribe => { unsub = unsubscribe; });
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next available Asset ID
|
||||
*/
|
||||
async function getNextAssetId(api: ApiPromise): Promise<number> {
|
||||
// Query existing assets and find the next ID
|
||||
// This is a simplified version - adjust based on your implementation
|
||||
const assets = await api.query.assets.asset.entries();
|
||||
|
||||
if (assets.length === 0) {
|
||||
return 1000; // Start from 1000 for foreign assets
|
||||
}
|
||||
|
||||
const maxId = Math.max(...assets.map(([key]) => {
|
||||
const assetId = key.args[0].toNumber();
|
||||
return assetId;
|
||||
}));
|
||||
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STEP 6: TEST XCM TRANSFER
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Test XCM transfer from Asset Hub USDT to our wUSDT
|
||||
*
|
||||
* @param api - Polkadot.js API instance (our parachain)
|
||||
* @param amount - Amount to transfer (in smallest unit)
|
||||
* @param account - Account to receive the transfer
|
||||
* @returns Test result with transaction hash and balance
|
||||
*/
|
||||
export async function testXCMTransfer(
|
||||
api: ApiPromise,
|
||||
amount: string,
|
||||
account: InjectedAccountWithMeta
|
||||
): Promise<XCMTestResult> {
|
||||
try {
|
||||
// This is a placeholder for XCM testing
|
||||
// In reality, you'd need to:
|
||||
// 1. Connect to Asset Hub
|
||||
// 2. Send limitedReserveTransferAssets() to our parachain
|
||||
// 3. Monitor for AssetReceived event on our side
|
||||
|
||||
// For now, return a mock success result
|
||||
return {
|
||||
txHash: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
success: false,
|
||||
balance: '0',
|
||||
error: 'XCM testing requires connection to relay chain and Asset Hub',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
txHash: '',
|
||||
success: false,
|
||||
balance: '0',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 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
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get relay chain endpoint based on network selection
|
||||
*/
|
||||
export function getRelayChainEndpoint(relayChain: RelayChain): string {
|
||||
const endpoints = {
|
||||
westend: 'wss://westend-rpc.polkadot.io',
|
||||
rococo: 'wss://rococo-rpc.polkadot.io',
|
||||
polkadot: 'wss://rpc.polkadot.io',
|
||||
};
|
||||
|
||||
return endpoints[relayChain];
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Hub ParaId by relay chain
|
||||
*/
|
||||
export function getAssetHubParaId(relayChain: RelayChain): number {
|
||||
const paraIds = {
|
||||
westend: 1000, // Westend Asset Hub
|
||||
rococo: 1000, // Rococo Asset Hub
|
||||
polkadot: 1000, // Polkadot Asset Hub (Statemint)
|
||||
};
|
||||
|
||||
return paraIds[relayChain];
|
||||
}
|
||||
Reference in new issue
Block a user