mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-12 17:51:02 +00:00
Fix USDT swap insufficient balance bug
Root cause: Token symbol mismatch between TokenSwap component and WalletContext - WalletContext stored balance with key 'USDT' - TokenSwap used symbol 'wUSDT' for lookups - This caused balances['wUSDT'] to return undefined - Triggered false "insufficient balance" errors Changes: - Updated TokenSwap.tsx to use 'USDT' symbol consistently - Fixed token symbol in AVAILABLE_TOKENS array - Updated asset ID mapping in swap transaction logic - Fixed swap history token display Also includes: - Added new DEX components (SwapInterface, PoolBrowser, etc.) - Added TypeScript type definitions for DEX - Added DEX utility functions - Removed obsolete test scripts - Updated WalletContext with USDT balance support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
|
|
||||||
console.log('Checking current asset state...\n');
|
|
||||||
|
|
||||||
// Check if asset 2 exists
|
|
||||||
console.log('Asset ID 2 (wUSDT):');
|
|
||||||
try {
|
|
||||||
const asset2 = await api.query.assets.asset(2);
|
|
||||||
if (asset2.isSome) {
|
|
||||||
console.log(' EXISTS:', asset2.unwrap().toHuman());
|
|
||||||
} else {
|
|
||||||
console.log(' DOES NOT EXIST');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' ERROR:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check asset 0 and 1
|
|
||||||
console.log('\nAsset ID 0 (wHEZ):');
|
|
||||||
try {
|
|
||||||
const asset0 = await api.query.assets.asset(0);
|
|
||||||
if (asset0.isSome) {
|
|
||||||
console.log(' EXISTS:', asset0.unwrap().toHuman());
|
|
||||||
} else {
|
|
||||||
console.log(' DOES NOT EXIST');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' ERROR:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nAsset ID 1 (PEZ):');
|
|
||||||
try {
|
|
||||||
const asset1 = await api.query.assets.asset(1);
|
|
||||||
if (asset1.isSome) {
|
|
||||||
console.log(' EXISTS:', asset1.unwrap().toHuman());
|
|
||||||
} else {
|
|
||||||
console.log(' DOES NOT EXIST');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' ERROR:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check next asset ID
|
|
||||||
console.log('\nNext Asset ID:');
|
|
||||||
try {
|
|
||||||
const nextId = await api.query.assets.nextAssetId();
|
|
||||||
console.log(' ', nextId.toHuman());
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' ERROR:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { Keyring } from '@polkadot/keyring';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('Founder address:', founder.address);
|
|
||||||
console.log('\nChecking balances...');
|
|
||||||
|
|
||||||
const whezBal = await api.query.assets.account(0, founder.address);
|
|
||||||
const pezBal = await api.query.assets.account(1, founder.address);
|
|
||||||
const wusdtBal = await api.query.assets.account(2, founder.address);
|
|
||||||
|
|
||||||
console.log(' wHEZ:', whezBal.isSome ? whezBal.unwrap().balance.toHuman() : '0');
|
|
||||||
console.log(' PEZ:', pezBal.isSome ? pezBal.unwrap().balance.toHuman() : '0');
|
|
||||||
console.log(' wUSDT:', wusdtBal.isSome ? wusdtBal.unwrap().balance.toHuman() : '0');
|
|
||||||
|
|
||||||
console.log('\nChecking pools...');
|
|
||||||
const pool1 = await api.query.assetConversion.pools([0, 1]);
|
|
||||||
const pool2 = await api.query.assetConversion.pools([0, 2]);
|
|
||||||
const pool3 = await api.query.assetConversion.pools([1, 2]);
|
|
||||||
|
|
||||||
console.log(' wHEZ/PEZ pool exists:', pool1.isSome);
|
|
||||||
console.log(' wHEZ/wUSDT pool exists:', pool2.isSome);
|
|
||||||
console.log(' PEZ/wUSDT pool exists:', pool3.isSome);
|
|
||||||
|
|
||||||
if (pool1.isSome) {
|
|
||||||
console.log('\nwHEZ/PEZ pool details:', pool1.unwrap().toHuman());
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('💰 Founder Balances\n');
|
|
||||||
console.log(`Address: ${founder.address}\n`);
|
|
||||||
|
|
||||||
// Check wHEZ (asset 0)
|
|
||||||
const whezBalance = await api.query.assets.account(0, founder.address);
|
|
||||||
if (whezBalance.isSome) {
|
|
||||||
const whez = Number(whezBalance.unwrap().balance.toString()) / 1e12;
|
|
||||||
console.log(`wHEZ: ${whez.toLocaleString()}`);
|
|
||||||
} else {
|
|
||||||
console.log('wHEZ: 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check PEZ (asset 1)
|
|
||||||
const pezBalance = await api.query.assets.account(1, founder.address);
|
|
||||||
if (pezBalance.isSome) {
|
|
||||||
const pez = Number(pezBalance.unwrap().balance.toString()) / 1e12;
|
|
||||||
console.log(`PEZ: ${pez.toLocaleString()}`);
|
|
||||||
} else {
|
|
||||||
console.log('PEZ: 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check wUSDT (asset 2)
|
|
||||||
const wusdtBalance = await api.query.assets.account(2, founder.address);
|
|
||||||
if (wusdtBalance.isSome) {
|
|
||||||
const wusdt = Number(wusdtBalance.unwrap().balance.toString()) / 1e6;
|
|
||||||
console.log(`wUSDT: ${wusdt.toLocaleString()}`);
|
|
||||||
} else {
|
|
||||||
console.log('wUSDT: 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n📊 Required for target pools:');
|
|
||||||
console.log('- wHEZ/wUSDT (4:1): 40,000 wHEZ + 10,000 wUSDT');
|
|
||||||
console.log('- PEZ/wUSDT (20:1): 200,000 PEZ + 10,000 wUSDT');
|
|
||||||
console.log('\n(Plus current pool balances need to be removed first)');
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { Keyring } from '@polkadot/keyring';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('Founder address:', founder.address);
|
|
||||||
|
|
||||||
// Check native HEZ balance
|
|
||||||
const { data: balance } = await api.query.system.account(founder.address);
|
|
||||||
console.log('\nNative HEZ balance:', balance.free.toHuman());
|
|
||||||
console.log('Reserved:', balance.reserved.toHuman());
|
|
||||||
console.log('Frozen:', balance.frozen.toHuman());
|
|
||||||
|
|
||||||
// Check wHEZ balance
|
|
||||||
const whezBal = await api.query.assets.account(0, founder.address);
|
|
||||||
console.log('\nwHEZ balance:', whezBal.isSome ? whezBal.unwrap().balance.toHuman() : '0');
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { cryptoWaitReady, blake2AsHex } from '@polkadot/util-crypto';
|
|
||||||
import { stringToU8a, u8aConcat, bnToU8a } from '@polkadot/util';
|
|
||||||
|
|
||||||
async function derivePoolAccount(api, asset1, asset2) {
|
|
||||||
// PalletId for AssetConversion: "py/ascon" (8 bytes)
|
|
||||||
const PALLET_ID = stringToU8a('py/ascon');
|
|
||||||
|
|
||||||
// Create PoolId tuple (u32, u32)
|
|
||||||
const poolId = api.createType('(u32, u32)', [asset1, asset2]);
|
|
||||||
|
|
||||||
// Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32))
|
|
||||||
const palletIdType = api.createType('[u8; 8]', PALLET_ID);
|
|
||||||
const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolId]);
|
|
||||||
|
|
||||||
// Hash the SCALE-encoded tuple using BLAKE2-256
|
|
||||||
const accountHash = blake2AsHex(fullTuple.toU8a(), 256);
|
|
||||||
|
|
||||||
// Create AccountId from the hash
|
|
||||||
const poolAccountId = api.createType('AccountId32', accountHash);
|
|
||||||
return poolAccountId.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
|
|
||||||
console.log('🔍 Checking pool balances in detail\n');
|
|
||||||
|
|
||||||
// Check wHEZ/wUSDT pool (0, 2)
|
|
||||||
console.log('=== Pool: wHEZ/wUSDT (0, 2) ===');
|
|
||||||
const pool1Info = await api.query.assetConversion.pools([0, 2]);
|
|
||||||
|
|
||||||
if (pool1Info.isSome) {
|
|
||||||
const lpToken = pool1Info.unwrap().toJSON().lpToken;
|
|
||||||
console.log(`LP Token ID: ${lpToken}`);
|
|
||||||
|
|
||||||
const poolAccount = await derivePoolAccount(api, 0, 2);
|
|
||||||
console.log(`Pool Account: ${poolAccount}`);
|
|
||||||
|
|
||||||
// Query wHEZ balance (asset 0)
|
|
||||||
const whezBalance = await api.query.assets.account(0, poolAccount);
|
|
||||||
if (whezBalance.isSome) {
|
|
||||||
const whez = Number(whezBalance.unwrap().balance.toString()) / 1e12;
|
|
||||||
console.log(`wHEZ in pool: ${whez.toLocaleString()}`);
|
|
||||||
} else {
|
|
||||||
console.log('wHEZ balance: 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query wUSDT balance (asset 2)
|
|
||||||
const wusdtBalance = await api.query.assets.account(2, poolAccount);
|
|
||||||
if (wusdtBalance.isSome) {
|
|
||||||
const wusdt = Number(wusdtBalance.unwrap().balance.toString()) / 1e6;
|
|
||||||
console.log(`wUSDT in pool: ${wusdt.toLocaleString()}`);
|
|
||||||
|
|
||||||
// Calculate rate
|
|
||||||
const whezRaw = whezBalance.isSome ? Number(whezBalance.unwrap().balance.toString()) / 1e12 : 0;
|
|
||||||
if (whezRaw > 0) {
|
|
||||||
console.log(`Rate: 1 wUSDT = ${(whezRaw / wusdt).toFixed(4)} wHEZ`);
|
|
||||||
console.log(`Rate: 1 wHEZ = ${(wusdt / whezRaw).toFixed(4)} wUSDT`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('wUSDT balance: 0');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('❌ Pool does not exist!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check PEZ/wUSDT pool (1, 2)
|
|
||||||
console.log('\n=== Pool: PEZ/wUSDT (1, 2) ===');
|
|
||||||
const pool2Info = await api.query.assetConversion.pools([1, 2]);
|
|
||||||
|
|
||||||
if (pool2Info.isSome) {
|
|
||||||
const lpToken = pool2Info.unwrap().toJSON().lpToken;
|
|
||||||
console.log(`LP Token ID: ${lpToken}`);
|
|
||||||
|
|
||||||
const poolAccount = await derivePoolAccount(api, 1, 2);
|
|
||||||
console.log(`Pool Account: ${poolAccount}`);
|
|
||||||
|
|
||||||
// Query PEZ balance (asset 1)
|
|
||||||
const pezBalance = await api.query.assets.account(1, poolAccount);
|
|
||||||
if (pezBalance.isSome) {
|
|
||||||
const pez = Number(pezBalance.unwrap().balance.toString()) / 1e12;
|
|
||||||
console.log(`PEZ in pool: ${pez.toLocaleString()}`);
|
|
||||||
} else {
|
|
||||||
console.log('PEZ balance: 0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query wUSDT balance (asset 2)
|
|
||||||
const wusdtBalance = await api.query.assets.account(2, poolAccount);
|
|
||||||
if (wusdtBalance.isSome) {
|
|
||||||
const wusdt = Number(wusdtBalance.unwrap().balance.toString()) / 1e6;
|
|
||||||
console.log(`wUSDT in pool: ${wusdt.toLocaleString()}`);
|
|
||||||
|
|
||||||
// Calculate rate
|
|
||||||
const pezRaw = pezBalance.isSome ? Number(pezBalance.unwrap().balance.toString()) / 1e12 : 0;
|
|
||||||
if (pezRaw > 0) {
|
|
||||||
console.log(`Rate: 1 wUSDT = ${(pezRaw / wusdt).toFixed(4)} PEZ`);
|
|
||||||
console.log(`Rate: 1 PEZ = ${(wusdt / pezRaw).toFixed(6)} wUSDT`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('wUSDT balance: 0');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('❌ Pool does not exist!');
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { Keyring } from '@polkadot/keyring';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('\n🚀 Creating all 3 beta testnet pools...\n');
|
|
||||||
console.log('Target exchange rates:');
|
|
||||||
console.log(' 1 wUSDT = 4 wHEZ = 20 PEZ\n');
|
|
||||||
|
|
||||||
// Pool 1: wHEZ/PEZ (1 wHEZ = 5 PEZ)
|
|
||||||
console.log('📝 Pool 1: wHEZ/PEZ');
|
|
||||||
console.log(' Ratio: 100,000 wHEZ : 500,000 PEZ (1:5)');
|
|
||||||
const whezPez_whez = BigInt(100_000) * BigInt(10 ** 12); // 100k wHEZ (12 decimals)
|
|
||||||
const whezPez_pez = BigInt(500_000) * BigInt(10 ** 12); // 500k PEZ (12 decimals)
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(0, 1) // wHEZ=0, PEZ=1
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create pool error:', dispatchError.toString());
|
|
||||||
// Pool might already exist, continue
|
|
||||||
} else {
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'PoolCreated') {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
0, 1, // wHEZ, PEZ
|
|
||||||
whezPez_whez.toString(),
|
|
||||||
whezPez_pez.toString(),
|
|
||||||
whezPez_whez.toString(), // min amounts (same as desired)
|
|
||||||
whezPez_pez.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to wHEZ/PEZ pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pool 2: wHEZ/wUSDT (4 wHEZ = 1 wUSDT, or 1 wHEZ = 0.25 wUSDT)
|
|
||||||
console.log('\n📝 Pool 2: wHEZ/wUSDT');
|
|
||||||
console.log(' Ratio: 40,000 wHEZ : 10,000 wUSDT (4:1)');
|
|
||||||
const whezUsdt_whez = BigInt(40_000) * BigInt(10 ** 12); // 40k wHEZ (12 decimals)
|
|
||||||
const whezUsdt_usdt = BigInt(10_000) * BigInt(10 ** 6); // 10k wUSDT (6 decimals)
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(0, 2) // wHEZ=0, wUSDT=2
|
|
||||||
.signAndSend(founder, ({ status, dispatchError }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create pool error:', dispatchError.toString());
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
0, 2, // wHEZ, wUSDT
|
|
||||||
whezUsdt_whez.toString(),
|
|
||||||
whezUsdt_usdt.toString(),
|
|
||||||
whezUsdt_whez.toString(),
|
|
||||||
whezUsdt_usdt.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to wHEZ/wUSDT pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pool 3: PEZ/wUSDT (20 PEZ = 1 wUSDT, or 1 PEZ = 0.05 wUSDT)
|
|
||||||
console.log('\n📝 Pool 3: PEZ/wUSDT');
|
|
||||||
console.log(' Ratio: 200,000 PEZ : 10,000 wUSDT (20:1)');
|
|
||||||
const pezUsdt_pez = BigInt(200_000) * BigInt(10 ** 12); // 200k PEZ (12 decimals)
|
|
||||||
const pezUsdt_usdt = BigInt(10_000) * BigInt(10 ** 6); // 10k wUSDT (6 decimals)
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(1, 2) // PEZ=1, wUSDT=2
|
|
||||||
.signAndSend(founder, ({ status, dispatchError }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create pool error:', dispatchError.toString());
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
1, 2, // PEZ, wUSDT
|
|
||||||
pezUsdt_pez.toString(),
|
|
||||||
pezUsdt_usdt.toString(),
|
|
||||||
pezUsdt_pez.toString(),
|
|
||||||
pezUsdt_usdt.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to PEZ/wUSDT pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n✅ All 3 pools created successfully!');
|
|
||||||
console.log('\nPool Summary:');
|
|
||||||
console.log(' 1. wHEZ/PEZ: 100k:500k (1 wHEZ = 5 PEZ)');
|
|
||||||
console.log(' 2. wHEZ/wUSDT: 40k:10k (4 wHEZ = 1 wUSDT)');
|
|
||||||
console.log(' 3. PEZ/wUSDT: 200k:10k (20 PEZ = 1 wUSDT)');
|
|
||||||
console.log('\nExchange rates: 1 wUSDT = 4 wHEZ = 20 PEZ ✓');
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { Keyring } from '@polkadot/keyring';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('\n📝 Creating PEZ/wUSDT pool');
|
|
||||||
console.log(' Ratio: 200,000 PEZ : 10,000 wUSDT (20:1)');
|
|
||||||
console.log(' Exchange rate: 1 wUSDT = 20 PEZ\n');
|
|
||||||
|
|
||||||
const pezAmount = BigInt(200_000) * BigInt(10 ** 12);
|
|
||||||
const wusdtAmount = BigInt(10_000) * BigInt(10 ** 6);
|
|
||||||
|
|
||||||
// Check if pool exists
|
|
||||||
const poolExists = await api.query.assetConversion.pools([1, 2]);
|
|
||||||
|
|
||||||
if (poolExists.isNone) {
|
|
||||||
console.log('Creating pool...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(1, 2)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('✓ Pool already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
1, 2,
|
|
||||||
pezAmount.toString(),
|
|
||||||
wusdtAmount.toString(),
|
|
||||||
pezAmount.toString(),
|
|
||||||
wusdtAmount.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
if (dispatchError.isModule) {
|
|
||||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
|
||||||
console.log(' Error:', decoded.name, '-', decoded.docs.join(' '));
|
|
||||||
} else {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
}
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to PEZ/wUSDT pool!');
|
|
||||||
console.log(' Amounts:', event.data.toHuman());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n✅ PEZ/wUSDT pool is ready!');
|
|
||||||
console.log('Exchange rate: 1 wUSDT = 20 PEZ ✓');
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import js from "@eslint/js";
|
|
||||||
import globals from "globals";
|
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
|
||||||
import reactRefresh from "eslint-plugin-react-refresh";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
{ ignores: ["dist"] },
|
|
||||||
{
|
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
||||||
files: ["**/*.{ts,tsx}"],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"react-hooks": reactHooks,
|
|
||||||
"react-refresh": reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
"react-refresh/only-export-components": [
|
|
||||||
"warn",
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE admin_roles ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Admins can view admin roles" ON admin_roles;
|
||||||
|
DROP POLICY IF EXISTS "Super admins can manage admin roles" ON admin_roles;
|
||||||
|
DROP POLICY IF EXISTS "authenticated_read_admin_roles" ON admin_roles;
|
||||||
|
|
||||||
|
CREATE POLICY "authenticated_read_admin_roles"
|
||||||
|
ON admin_roles FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (true);
|
||||||
+36907
File diff suppressed because it is too large
Load Diff
@@ -1,237 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { Keyring } from '@polkadot/keyring';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('\n🔍 Checking founder token balances...');
|
|
||||||
console.log('Founder address:', founder.address);
|
|
||||||
|
|
||||||
// Check balances
|
|
||||||
const whezBalance = await api.query.assets.account(0, founder.address);
|
|
||||||
const pezBalance = await api.query.assets.account(1, founder.address);
|
|
||||||
const wusdtBalance = await api.query.assets.account(2, founder.address);
|
|
||||||
|
|
||||||
console.log('\nCurrent balances:');
|
|
||||||
console.log(' wHEZ:', whezBalance.isSome ? whezBalance.unwrap().balance.toString() : '0');
|
|
||||||
console.log(' PEZ:', pezBalance.isSome ? pezBalance.unwrap().balance.toString() : '0');
|
|
||||||
console.log(' wUSDT:', wusdtBalance.isSome ? wusdtBalance.unwrap().balance.toString() : '0');
|
|
||||||
|
|
||||||
// Mint wHEZ to founder using sudo
|
|
||||||
console.log('\n💰 Minting 200,000 wHEZ to founder via sudo...');
|
|
||||||
const whezAmount = BigInt(200_000) * BigInt(10 ** 12);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.sudo
|
|
||||||
.sudo(
|
|
||||||
api.tx.assets.mint(0, founder.address, whezAmount.toString())
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(' ✓ wHEZ minted successfully');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n🚀 Creating all 3 beta testnet pools...\n');
|
|
||||||
console.log('Target exchange rates:');
|
|
||||||
console.log(' 1 wUSDT = 4 wHEZ = 20 PEZ\n');
|
|
||||||
|
|
||||||
// Pool 1: wHEZ/PEZ (1 wHEZ = 5 PEZ)
|
|
||||||
console.log('📝 Pool 1: wHEZ/PEZ');
|
|
||||||
console.log(' Ratio: 100,000 wHEZ : 500,000 PEZ (1:5)');
|
|
||||||
const whezPez_whez = BigInt(100_000) * BigInt(10 ** 12);
|
|
||||||
const whezPez_pez = BigInt(500_000) * BigInt(10 ** 12);
|
|
||||||
|
|
||||||
// Check if pool exists
|
|
||||||
const pool1 = await api.query.assetConversion.pools([0, 1]);
|
|
||||||
if (pool1.isNone) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(0, 1)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create pool error:', dispatchError.toString());
|
|
||||||
} else {
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'PoolCreated') {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
0, 1,
|
|
||||||
whezPez_whez.toString(),
|
|
||||||
whezPez_pez.toString(),
|
|
||||||
whezPez_whez.toString(),
|
|
||||||
whezPez_pez.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to wHEZ/PEZ pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pool 2: wHEZ/wUSDT (4:1)
|
|
||||||
console.log('\n📝 Pool 2: wHEZ/wUSDT');
|
|
||||||
console.log(' Ratio: 40,000 wHEZ : 10,000 wUSDT (4:1)');
|
|
||||||
const whezUsdt_whez = BigInt(40_000) * BigInt(10 ** 12);
|
|
||||||
const whezUsdt_usdt = BigInt(10_000) * BigInt(10 ** 6);
|
|
||||||
|
|
||||||
const pool2 = await api.query.assetConversion.pools([0, 2]);
|
|
||||||
if (pool2.isNone) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(0, 2)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create pool error:', dispatchError.toString());
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
0, 2,
|
|
||||||
whezUsdt_whez.toString(),
|
|
||||||
whezUsdt_usdt.toString(),
|
|
||||||
whezUsdt_whez.toString(),
|
|
||||||
whezUsdt_usdt.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to wHEZ/wUSDT pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pool 3: PEZ/wUSDT (20:1)
|
|
||||||
console.log('\n📝 Pool 3: PEZ/wUSDT');
|
|
||||||
console.log(' Ratio: 200,000 PEZ : 10,000 wUSDT (20:1)');
|
|
||||||
const pezUsdt_pez = BigInt(200_000) * BigInt(10 ** 12);
|
|
||||||
const pezUsdt_usdt = BigInt(10_000) * BigInt(10 ** 6);
|
|
||||||
|
|
||||||
const pool3 = await api.query.assetConversion.pools([1, 2]);
|
|
||||||
if (pool3.isNone) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(1, 2)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create pool error:', dispatchError.toString());
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
1, 2,
|
|
||||||
pezUsdt_pez.toString(),
|
|
||||||
pezUsdt_usdt.toString(),
|
|
||||||
pezUsdt_pez.toString(),
|
|
||||||
pezUsdt_usdt.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to PEZ/wUSDT pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n✅ All 3 pools created successfully!');
|
|
||||||
console.log('\nPool Summary:');
|
|
||||||
console.log(' 1. wHEZ/PEZ: 100k:500k (1 wHEZ = 5 PEZ)');
|
|
||||||
console.log(' 2. wHEZ/wUSDT: 40k:10k (4 wHEZ = 1 wUSDT)');
|
|
||||||
console.log(' 3. PEZ/wUSDT: 200k:10k (20 PEZ = 1 wUSDT)');
|
|
||||||
console.log('\nExchange rates: 1 wUSDT = 4 wHEZ = 20 PEZ ✓');
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { Keyring } from '@polkadot/keyring';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('Minting 200,000 wHEZ to founder via sudo...');
|
|
||||||
const whezAmount = BigInt(200_000) * BigInt(10 ** 12);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.sudo
|
|
||||||
.sudo(
|
|
||||||
api.tx.assets.mint(0, founder.address, whezAmount.toString())
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
console.log('Transaction status:', status.type);
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
if (dispatchError.isModule) {
|
|
||||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
|
||||||
console.log(' Error:', decoded.name, '-', decoded.docs.join(' '));
|
|
||||||
} else {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
}
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
console.log(' Event:', event.section + '.' + event.method);
|
|
||||||
if (event.section === 'sudo' && event.method === 'Sudid') {
|
|
||||||
const result = event.data[0];
|
|
||||||
if (result.isOk) {
|
|
||||||
console.log(' ✓ Sudo call succeeded');
|
|
||||||
} else {
|
|
||||||
console.log(' ✗ Sudo call failed:', result.asErr.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nChecking balance after mint...');
|
|
||||||
const whezBal = await api.query.assets.account(0, founder.address);
|
|
||||||
console.log('wHEZ balance:', whezBal.isSome ? whezBal.unwrap().balance.toHuman() : '0');
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -48,6 +48,9 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
const [amount0, setAmount0] = useState('');
|
const [amount0, setAmount0] = useState('');
|
||||||
const [amount1, setAmount1] = useState('');
|
const [amount1, setAmount1] = useState('');
|
||||||
const [currentPrice, setCurrentPrice] = useState<number | null>(null);
|
const [currentPrice, setCurrentPrice] = useState<number | null>(null);
|
||||||
|
const [isPoolEmpty, setIsPoolEmpty] = useState(true); // Track if pool has meaningful liquidity
|
||||||
|
const [minDeposit0, setMinDeposit0] = useState<number>(0.01); // Dynamic minimum deposit for asset0
|
||||||
|
const [minDeposit1, setMinDeposit1] = useState<number>(0.01); // Dynamic minimum deposit for asset1
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
@@ -60,6 +63,82 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
const asset0Decimals = getAssetDecimals(asset0);
|
const asset0Decimals = getAssetDecimals(asset0);
|
||||||
const asset1Decimals = getAssetDecimals(asset1);
|
const asset1Decimals = getAssetDecimals(asset1);
|
||||||
|
|
||||||
|
// Reset form when modal is closed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setAmount0('');
|
||||||
|
setAmount1('');
|
||||||
|
setError(null);
|
||||||
|
setSuccess(false);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Fetch minimum deposit requirements from runtime
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api || !isApiReady || !isOpen) return;
|
||||||
|
|
||||||
|
const fetchMinimumBalances = async () => {
|
||||||
|
try {
|
||||||
|
// Query asset details which contains minBalance
|
||||||
|
const assetDetails0 = await api.query.assets.asset(asset0);
|
||||||
|
const assetDetails1 = await api.query.assets.asset(asset1);
|
||||||
|
|
||||||
|
console.log('🔍 Querying minimum balances for assets:', { asset0, asset1 });
|
||||||
|
|
||||||
|
if (assetDetails0.isSome && assetDetails1.isSome) {
|
||||||
|
const details0 = assetDetails0.unwrap().toJSON() as any;
|
||||||
|
const details1 = assetDetails1.unwrap().toJSON() as any;
|
||||||
|
|
||||||
|
console.log('📦 Asset details:', {
|
||||||
|
asset0: details0,
|
||||||
|
asset1: details1
|
||||||
|
});
|
||||||
|
|
||||||
|
const minBalance0Raw = details0.minBalance || '0';
|
||||||
|
const minBalance1Raw = details1.minBalance || '0';
|
||||||
|
|
||||||
|
const minBalance0 = Number(minBalance0Raw) / Math.pow(10, asset0Decimals);
|
||||||
|
const minBalance1 = Number(minBalance1Raw) / Math.pow(10, asset1Decimals);
|
||||||
|
|
||||||
|
console.log('📊 Minimum deposit requirements from assets:', {
|
||||||
|
asset0: asset0Name,
|
||||||
|
minBalance0Raw,
|
||||||
|
minBalance0,
|
||||||
|
asset1: asset1Name,
|
||||||
|
minBalance1Raw,
|
||||||
|
minBalance1
|
||||||
|
});
|
||||||
|
|
||||||
|
setMinDeposit0(minBalance0);
|
||||||
|
setMinDeposit1(minBalance1);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Asset details not found, using defaults');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if there's a MintMinLiquidity constant in assetConversion pallet
|
||||||
|
if (api.consts.assetConversion) {
|
||||||
|
const mintMinLiq = api.consts.assetConversion.mintMinLiquidity;
|
||||||
|
if (mintMinLiq) {
|
||||||
|
console.log('🔧 AssetConversion MintMinLiquidity constant:', mintMinLiq.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const liquidityWithdrawalFee = api.consts.assetConversion.liquidityWithdrawalFee;
|
||||||
|
if (liquidityWithdrawalFee) {
|
||||||
|
console.log('🔧 AssetConversion LiquidityWithdrawalFee:', liquidityWithdrawalFee.toHuman());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log all assetConversion constants
|
||||||
|
console.log('🔧 All assetConversion constants:', Object.keys(api.consts.assetConversion));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Error fetching minimum balances:', err);
|
||||||
|
// Keep default 0.01 if query fails
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMinimumBalances();
|
||||||
|
}, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals, asset0Name, asset1Name]);
|
||||||
|
|
||||||
// Fetch current pool price
|
// Fetch current pool price
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!api || !isApiReady || !isOpen) return;
|
if (!api || !isApiReady || !isOpen) return;
|
||||||
@@ -93,26 +172,50 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
const reserve0 = Number(data0.balance) / Math.pow(10, asset0Decimals);
|
const reserve0 = Number(data0.balance) / Math.pow(10, asset0Decimals);
|
||||||
const reserve1 = Number(data1.balance) / Math.pow(10, asset1Decimals);
|
const reserve1 = Number(data1.balance) / Math.pow(10, asset1Decimals);
|
||||||
|
|
||||||
setCurrentPrice(reserve1 / reserve0);
|
// Consider pool empty if reserves are less than 1 token (dust amounts)
|
||||||
|
const MINIMUM_LIQUIDITY = 1;
|
||||||
|
if (reserve0 >= MINIMUM_LIQUIDITY && reserve1 >= MINIMUM_LIQUIDITY) {
|
||||||
|
setCurrentPrice(reserve1 / reserve0);
|
||||||
|
setIsPoolEmpty(false);
|
||||||
|
console.log('Pool has liquidity - auto-calculating ratio:', reserve1 / reserve0);
|
||||||
|
} else {
|
||||||
|
setCurrentPrice(null);
|
||||||
|
setIsPoolEmpty(true);
|
||||||
|
console.log('Pool is empty or has dust only - manual input allowed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No reserves found - pool is empty
|
||||||
|
setCurrentPrice(null);
|
||||||
|
setIsPoolEmpty(true);
|
||||||
|
console.log('Pool is empty - manual input allowed');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Pool doesn't exist yet - completely empty
|
||||||
|
setCurrentPrice(null);
|
||||||
|
setIsPoolEmpty(true);
|
||||||
|
console.log('Pool does not exist yet - manual input allowed');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching pool price:', err);
|
console.error('Error fetching pool price:', err);
|
||||||
|
// On error, assume pool is empty to allow manual input
|
||||||
|
setCurrentPrice(null);
|
||||||
|
setIsPoolEmpty(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchPoolPrice();
|
fetchPoolPrice();
|
||||||
}, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals]);
|
}, [api, isApiReady, isOpen, asset0, asset1, asset0Decimals, asset1Decimals]);
|
||||||
|
|
||||||
// Auto-calculate asset1 amount based on asset0 input
|
// Auto-calculate asset1 amount based on asset0 input (only if pool has liquidity)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (amount0 && currentPrice) {
|
if (!isPoolEmpty && amount0 && currentPrice) {
|
||||||
const calculated = parseFloat(amount0) * currentPrice;
|
const calculated = parseFloat(amount0) * currentPrice;
|
||||||
setAmount1(calculated.toFixed(asset1Decimals === 6 ? 2 : 4));
|
setAmount1(calculated.toFixed(asset1Decimals === 6 ? 2 : 4));
|
||||||
} else if (!amount0) {
|
} else if (!amount0 && !isPoolEmpty) {
|
||||||
setAmount1('');
|
setAmount1('');
|
||||||
}
|
}
|
||||||
}, [amount0, currentPrice, asset1Decimals]);
|
// If pool is empty, don't auto-calculate - let user input both amounts
|
||||||
|
}, [amount0, currentPrice, asset1Decimals, isPoolEmpty]);
|
||||||
|
|
||||||
const handleAddLiquidity = async () => {
|
const handleAddLiquidity = async () => {
|
||||||
if (!api || !selectedAccount || !amount0 || !amount1) return;
|
if (!api || !selectedAccount || !amount0 || !amount1) return;
|
||||||
@@ -128,6 +231,19 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check minimum deposit requirements from runtime
|
||||||
|
if (parseFloat(amount0) < minDeposit0) {
|
||||||
|
setError(`${asset0Name} amount must be at least ${minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} (minimum deposit requirement)`);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseFloat(amount1) < minDeposit1) {
|
||||||
|
setError(`${asset1Name} amount must be at least ${minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} (minimum deposit requirement)`);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const balance0 = (balances as any)[asset0BalanceKey] || 0;
|
const balance0 = (balances as any)[asset0BalanceKey] || 0;
|
||||||
const balance1 = (balances as any)[asset1BalanceKey] || 0;
|
const balance1 = (balances as any)[asset1BalanceKey] || 0;
|
||||||
|
|
||||||
@@ -277,26 +393,41 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Alert className="mb-4 bg-blue-900/20 border-blue-500">
|
{isPoolEmpty ? (
|
||||||
<Info className="h-4 w-4" />
|
<Alert className="mb-4 bg-yellow-900/20 border-yellow-500">
|
||||||
<AlertDescription className="text-sm">
|
<AlertCircle className="h-4 w-4" />
|
||||||
Add liquidity to earn 3% fees from all swaps.
|
<AlertDescription className="text-sm">
|
||||||
{(asset0 === 0 || asset0 === ASSET_IDS.WHEZ) && ' Your HEZ will be automatically wrapped to wHEZ.'}
|
<strong>First Liquidity Provider:</strong> Pool is empty! You are setting the initial price ratio.
|
||||||
</AlertDescription>
|
<strong> Minimum deposit: {minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} {asset0Name} and {minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name}.</strong>
|
||||||
</Alert>
|
{(asset0 === 0 || asset0 === ASSET_IDS.WHEZ) && ' Your HEZ will be automatically wrapped to wHEZ.'}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert className="mb-4 bg-blue-900/20 border-blue-500">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription className="text-sm">
|
||||||
|
Add liquidity to earn 3% fees from all swaps. Amounts are auto-calculated based on current pool ratio.
|
||||||
|
<strong> Minimum deposit: {minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} {asset0Name} and {minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name}.</strong>
|
||||||
|
{(asset0 === 0 || asset0 === ASSET_IDS.WHEZ) && ' Your HEZ will be automatically wrapped to wHEZ.'}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Asset 0 Input */}
|
{/* Asset 0 Input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
{asset0Name} Amount
|
{asset0Name} Amount
|
||||||
|
<span className="text-xs text-gray-500 ml-2">(min: {minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)})</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={amount0}
|
value={amount0}
|
||||||
onChange={(e) => setAmount0(e.target.value)}
|
onChange={(e) => setAmount0(e.target.value)}
|
||||||
placeholder="0.0"
|
placeholder={`${minDeposit0.toFixed(asset0Decimals === 6 ? 2 : 4)} or more`}
|
||||||
|
min={minDeposit0}
|
||||||
|
step={minDeposit0 < 1 ? minDeposit0 : 0.01}
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500"
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
@@ -322,16 +453,29 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
{/* Asset 1 Input */}
|
{/* Asset 1 Input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
{asset1Name} Amount (Auto-calculated)
|
{asset1Name} Amount {!isPoolEmpty && '(Auto-calculated)'}
|
||||||
|
{isPoolEmpty && (
|
||||||
|
<>
|
||||||
|
<span className="text-yellow-400 text-xs ml-2">⚠️ You set the initial ratio</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">(min: {minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)})</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={amount1}
|
value={amount1}
|
||||||
placeholder="0.0"
|
onChange={(e) => setAmount1(e.target.value)}
|
||||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-gray-400 focus:outline-none cursor-not-allowed"
|
placeholder={isPoolEmpty ? `${minDeposit1.toFixed(asset1Decimals === 6 ? 2 : 4)} or more` : "Auto-calculated"}
|
||||||
disabled={true}
|
min={isPoolEmpty ? minDeposit1 : undefined}
|
||||||
readOnly
|
step={isPoolEmpty ? (minDeposit1 < 1 ? minDeposit1 : 0.01) : undefined}
|
||||||
|
className={`w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 focus:outline-none ${
|
||||||
|
isPoolEmpty
|
||||||
|
? 'text-white focus:border-blue-500'
|
||||||
|
: 'text-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
disabled={!isPoolEmpty || isLoading}
|
||||||
|
readOnly={!isPoolEmpty}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-3 top-3 flex items-center gap-2">
|
<div className="absolute right-3 top-3 flex items-center gap-2">
|
||||||
<span className="text-gray-400 text-sm">{asset1Name}</span>
|
<span className="text-gray-400 text-sm">{asset1Name}</span>
|
||||||
@@ -339,18 +483,33 @@ export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-1 text-xs text-gray-400">
|
<div className="flex justify-between mt-1 text-xs text-gray-400">
|
||||||
<span>Balance: {balance1.toLocaleString()}</span>
|
<span>Balance: {balance1.toLocaleString()}</span>
|
||||||
<span>
|
{isPoolEmpty ? (
|
||||||
{currentPrice && `Rate: 1 ${asset0Name} = ${currentPrice.toFixed(asset1Decimals === 6 ? 2 : 4)} ${asset1Name}`}
|
<button
|
||||||
</span>
|
onClick={() => setAmount1(balance1.toString())}
|
||||||
|
className="text-blue-400 hover:text-blue-300"
|
||||||
|
>
|
||||||
|
Max
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
currentPrice && <span>Rate: 1 {asset0Name} = {currentPrice.toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price Info */}
|
{/* Price Info */}
|
||||||
{amount0 && amount1 && (
|
{amount0 && amount1 && (
|
||||||
<div className="bg-gray-800 rounded-lg p-3 space-y-2 text-sm">
|
<div className="bg-gray-800 rounded-lg p-3 space-y-2 text-sm">
|
||||||
|
{isPoolEmpty && (
|
||||||
|
<div className="flex justify-between text-yellow-300">
|
||||||
|
<span>Initial Price</span>
|
||||||
|
<span>
|
||||||
|
1 {asset0Name} = {(parseFloat(amount1) / parseFloat(amount0)).toFixed(asset1Decimals === 6 ? 2 : 4)} {asset1Name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex justify-between text-gray-300">
|
<div className="flex justify-between text-gray-300">
|
||||||
<span>Share of Pool</span>
|
<span>Share of Pool</span>
|
||||||
<span>~0.1%</span>
|
<span>{isPoolEmpty ? '100%' : '~0.1%'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-gray-300">
|
<div className="flex justify-between text-gray-300">
|
||||||
<span>Slippage Tolerance</span>
|
<span>Slippage Tolerance</span>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import PalletsGrid from './PalletsGrid';
|
|||||||
import TeamSection from './TeamSection';
|
import TeamSection from './TeamSection';
|
||||||
import ChainSpecs from './ChainSpecs';
|
import ChainSpecs from './ChainSpecs';
|
||||||
import TrustScoreCalculator from './TrustScoreCalculator';
|
import TrustScoreCalculator from './TrustScoreCalculator';
|
||||||
|
import { NetworkStats } from './NetworkStats';
|
||||||
import { WalletButton } from './wallet/WalletButton';
|
import { WalletButton } from './wallet/WalletButton';
|
||||||
import { WalletModal } from './wallet/WalletModal';
|
import { WalletModal } from './wallet/WalletModal';
|
||||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||||
@@ -21,7 +22,7 @@ import { TreasuryOverview } from './treasury/TreasuryOverview';
|
|||||||
import { FundingProposal } from './treasury/FundingProposal';
|
import { FundingProposal } from './treasury/FundingProposal';
|
||||||
import { SpendingHistory } from './treasury/SpendingHistory';
|
import { SpendingHistory } from './treasury/SpendingHistory';
|
||||||
import { MultiSigApproval } from './treasury/MultiSigApproval';
|
import { MultiSigApproval } from './treasury/MultiSigApproval';
|
||||||
import { Github, FileText, ExternalLink, Shield, Award, User, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, UserCog, Repeat, Users } from 'lucide-react';
|
import { Github, FileText, ExternalLink, Shield, Award, User, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, UserCog, Repeat, Users, Droplet } from 'lucide-react';
|
||||||
import GovernanceInterface from './GovernanceInterface';
|
import GovernanceInterface from './GovernanceInterface';
|
||||||
import RewardDistribution from './RewardDistribution';
|
import RewardDistribution from './RewardDistribution';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
@@ -32,6 +33,7 @@ import { MultiSigWallet } from './wallet/MultiSigWallet';
|
|||||||
import { useWallet } from '@/contexts/WalletContext';
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { PolkadotWalletButton } from './PolkadotWalletButton';
|
import { PolkadotWalletButton } from './PolkadotWalletButton';
|
||||||
|
import { DEXDashboard } from './dex/DEXDashboard';
|
||||||
const AppLayout: React.FC = () => {
|
const AppLayout: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [walletModalOpen, setWalletModalOpen] = useState(false);
|
const [walletModalOpen, setWalletModalOpen] = useState(false);
|
||||||
@@ -47,6 +49,7 @@ const AppLayout: React.FC = () => {
|
|||||||
const [showP2P, setShowP2P] = useState(false);
|
const [showP2P, setShowP2P] = useState(false);
|
||||||
const [showMultiSig, setShowMultiSig] = useState(false);
|
const [showMultiSig, setShowMultiSig] = useState(false);
|
||||||
const [showTokenSwap, setShowTokenSwap] = useState(false);
|
const [showTokenSwap, setShowTokenSwap] = useState(false);
|
||||||
|
const [showDEX, setShowDEX] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isConnected } = useWebSocket();
|
const { isConnected } = useWebSocket();
|
||||||
const { account } = useWallet();
|
const { account } = useWallet();
|
||||||
@@ -56,13 +59,18 @@ const AppLayout: React.FC = () => {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const checkAdminStatus = async () => {
|
const checkAdminStatus = async () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
const { data } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('admin_roles')
|
.from('admin_roles')
|
||||||
.select('role')
|
.select('role')
|
||||||
.eq('user_id', user.id)
|
.eq('user_id', user.id)
|
||||||
.single();
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.warn('Admin check error:', error);
|
||||||
|
}
|
||||||
setIsAdmin(!!data);
|
setIsAdmin(!!data);
|
||||||
|
} else {
|
||||||
|
setIsAdmin(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
checkAdminStatus();
|
checkAdminStatus();
|
||||||
@@ -123,21 +131,21 @@ const AppLayout: React.FC = () => {
|
|||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg"
|
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg"
|
||||||
>
|
>
|
||||||
<FileEdit className="w-4 h-4" />
|
<FileEdit className="w-4 h-4" />
|
||||||
{t('nav.proposals')}
|
Proposals
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDelegation(true)}
|
onClick={() => setShowDelegation(true)}
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Users2 className="w-4 h-4" />
|
<Users2 className="w-4 h-4" />
|
||||||
{t('nav.delegation')}
|
Delegation
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowForum(true)}
|
onClick={() => setShowForum(true)}
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-4 h-4" />
|
<MessageSquare className="w-4 h-4" />
|
||||||
{t('nav.forum')}
|
Forum
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -147,14 +155,14 @@ const AppLayout: React.FC = () => {
|
|||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<PiggyBank className="w-4 h-4" />
|
<PiggyBank className="w-4 h-4" />
|
||||||
{t('nav.treasury')}
|
Treasury
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowModeration(true)}
|
onClick={() => setShowModeration(true)}
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg"
|
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-b-lg"
|
||||||
>
|
>
|
||||||
<ShieldCheck className="w-4 h-4" />
|
<ShieldCheck className="w-4 h-4" />
|
||||||
{t('nav.moderation')}
|
Moderation
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,8 +178,15 @@ const AppLayout: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<div className="absolute left-0 mt-2 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
<div className="absolute left-0 mt-2 w-48 bg-gray-900 border border-gray-700 rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowTokenSwap(true)}
|
onClick={() => setShowDEX(true)}
|
||||||
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg"
|
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2 rounded-t-lg"
|
||||||
|
>
|
||||||
|
<Droplet className="w-4 h-4" />
|
||||||
|
DEX Pools
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTokenSwap(true)}
|
||||||
|
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Repeat className="w-4 h-4" />
|
<Repeat className="w-4 h-4" />
|
||||||
Token Swap
|
Token Swap
|
||||||
@@ -269,7 +284,9 @@ const AppLayout: React.FC = () => {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main>
|
<main>
|
||||||
{/* Conditional Rendering for Features */}
|
{/* Conditional Rendering for Features */}
|
||||||
{showProposalWizard ? (
|
{showDEX ? (
|
||||||
|
<DEXDashboard />
|
||||||
|
) : showProposalWizard ? (
|
||||||
<ProposalWizard
|
<ProposalWizard
|
||||||
onComplete={(proposal) => {
|
onComplete={(proposal) => {
|
||||||
console.log('Proposal created:', proposal);
|
console.log('Proposal created:', proposal);
|
||||||
@@ -400,6 +417,7 @@ const AppLayout: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
<NetworkStats key="network-stats-live" />
|
||||||
<PalletsGrid />
|
<PalletsGrid />
|
||||||
<TokenomicsSection />
|
<TokenomicsSection />
|
||||||
|
|
||||||
@@ -420,10 +438,11 @@ const AppLayout: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{(showProposalWizard || showDelegation || showForum || showModeration || showTreasury || showStaking || showP2P || showMultiSig || showTokenSwap) && (
|
{(showDEX || showProposalWizard || showDelegation || showForum || showModeration || showTreasury || showStaking || showP2P || showMultiSig || showTokenSwap) && (
|
||||||
<div className="fixed bottom-8 right-8 z-50">
|
<div className="fixed bottom-8 right-8 z-50">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setShowDEX(false);
|
||||||
setShowProposalWizard(false);
|
setShowProposalWizard(false);
|
||||||
setShowDelegation(false);
|
setShowDelegation(false);
|
||||||
setShowForum(false);
|
setShowForum(false);
|
||||||
@@ -436,7 +455,7 @@ const AppLayout: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-full shadow-lg flex items-center gap-2 transition-all"
|
className="bg-green-600 hover:bg-green-700 text-white px-6 py-3 rounded-full shadow-lg flex items-center gap-2 transition-all"
|
||||||
>
|
>
|
||||||
← {t('common.backToHome')}
|
← Back to Home
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChevronRight, Cpu, GitBranch, Shield } from 'lucide-react';
|
import { ChevronRight, Shield } from 'lucide-react';
|
||||||
import { NetworkStats } from './NetworkStats';
|
|
||||||
import { usePolkadot } from '../contexts/PolkadotContext';
|
import { usePolkadot } from '../contexts/PolkadotContext';
|
||||||
import { formatBalance } from '../lib/wallet';
|
import { formatBalance } from '../lib/wallet';
|
||||||
|
|
||||||
@@ -93,57 +92,52 @@ const HeroSection: React.FC = () => {
|
|||||||
<div className="relative z-10 w-full text-center">
|
<div className="relative z-10 w-full text-center">
|
||||||
<div className="mb-8 inline-flex items-center px-4 py-2 rounded-full bg-green-600/20 backdrop-blur-sm border border-green-500/30">
|
<div className="mb-8 inline-flex items-center px-4 py-2 rounded-full bg-green-600/20 backdrop-blur-sm border border-green-500/30">
|
||||||
<Shield className="w-4 h-4 text-yellow-400 mr-2" />
|
<Shield className="w-4 h-4 text-yellow-400 mr-2" />
|
||||||
<span className="text-yellow-400 text-sm font-medium">Substrate Parachain v1.0</span>
|
<span className="text-yellow-400 text-sm font-medium">Digital Kurdistan State v1.0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||||
PezkuwiChain
|
PezkuwiChain
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-xl md:text-2xl text-gray-300 mb-8 max-w-3xl mx-auto">
|
<p className="text-xl md:text-2xl text-gray-300 mb-4 max-w-3xl mx-auto">
|
||||||
{t('hero.title')}
|
{t('hero.title', 'Blockchain Governance Platform')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
<p className="text-lg text-gray-400 mb-12 max-w-2xl mx-auto">
|
||||||
{t('hero.subtitle')}
|
{t('hero.subtitle', 'Democratic and transparent governance with blockchain technology')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Live Network Stats */}
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12 max-w-5xl mx-auto px-4">
|
||||||
<div className="mb-12">
|
<div className="bg-gray-900/70 backdrop-blur-md rounded-xl border border-green-500/40 p-6 hover:border-green-400/60 transition-all">
|
||||||
<NetworkStats />
|
<div className="text-4xl font-bold text-green-400 mb-2">{stats.activeProposals}</div>
|
||||||
</div>
|
<div className="text-sm text-gray-300 font-medium">{t('hero.stats.activeProposals', 'Active Proposals')}</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12 max-w-4xl mx-auto">
|
|
||||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-green-500/30 p-4">
|
|
||||||
<div className="text-2xl font-bold text-green-400">{stats.activeProposals}</div>
|
|
||||||
<div className="text-sm text-gray-400">{t('hero.stats.activeProposals')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-yellow-400/30 p-4">
|
<div className="bg-gray-900/70 backdrop-blur-md rounded-xl border border-yellow-400/40 p-6 hover:border-yellow-400/60 transition-all">
|
||||||
<div className="text-2xl font-bold text-yellow-400">{stats.totalVoters || '-'}</div>
|
<div className="text-4xl font-bold text-yellow-400 mb-2">{stats.totalVoters.toLocaleString()}</div>
|
||||||
<div className="text-sm text-gray-400">{t('hero.stats.totalVoters')}</div>
|
<div className="text-sm text-gray-300 font-medium">{t('hero.stats.totalVoters', 'Total Voters')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-red-500/30 p-4">
|
<div className="bg-gray-900/70 backdrop-blur-md rounded-xl border border-red-500/40 p-6 hover:border-red-500/60 transition-all">
|
||||||
<div className="text-2xl font-bold text-red-400">{stats.tokensStaked || '-'}</div>
|
<div className="text-4xl font-bold text-red-400 mb-2">{stats.tokensStaked}</div>
|
||||||
<div className="text-sm text-gray-400">{t('hero.stats.tokensStaked')}</div>
|
<div className="text-sm text-gray-300 font-medium">{t('hero.stats.tokensStaked', 'Tokens Staked')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-green-500/30 p-4">
|
<div className="bg-gray-900/70 backdrop-blur-md rounded-xl border border-green-500/40 p-6 hover:border-green-500/60 transition-all">
|
||||||
<div className="text-2xl font-bold text-green-400">{stats.trustScore ? `${stats.trustScore}%` : '-'}</div>
|
<div className="text-4xl font-bold text-green-400 mb-2">{stats.trustScore}%</div>
|
||||||
<div className="text-sm text-gray-400">{t('hero.stats.trustScore')}</div>
|
<div className="text-sm text-gray-300 font-medium">{t('hero.stats.trustScore', 'Trust Score')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center px-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => document.getElementById('governance')?.scrollIntoView({ behavior: 'smooth' })}
|
onClick={() => document.getElementById('governance')?.scrollIntoView({ behavior: 'smooth' })}
|
||||||
className="px-8 py-4 bg-gradient-to-r from-green-600 to-yellow-400 text-white font-semibold rounded-lg hover:from-green-700 hover:to-yellow-500 transition-all transform hover:scale-105 flex items-center justify-center group"
|
className="px-8 py-4 bg-gradient-to-r from-green-500 via-yellow-400 to-yellow-500 text-gray-900 font-bold rounded-lg hover:shadow-lg hover:shadow-yellow-400/50 transition-all transform hover:scale-105 flex items-center justify-center group"
|
||||||
>
|
>
|
||||||
{t('hero.exploreGovernance')}
|
{t('hero.exploreGovernance', 'Explore Governance')}
|
||||||
<ChevronRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
<ChevronRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => document.getElementById('identity')?.scrollIntoView({ behavior: 'smooth' })}
|
onClick={() => document.getElementById('governance')?.scrollIntoView({ behavior: 'smooth' })}
|
||||||
className="px-8 py-4 bg-gray-900/50 backdrop-blur-sm text-white font-semibold rounded-lg border border-red-500/50 hover:bg-red-500/10 transition-all"
|
className="px-8 py-4 bg-gray-900/80 backdrop-blur-sm text-white font-semibold rounded-lg border border-gray-700 hover:bg-gray-800 hover:border-gray-600 transition-all"
|
||||||
>
|
>
|
||||||
{t('hero.learnMore')}
|
{t('hero.learnMore', 'Learn More')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const NetworkStats: React.FC = () => {
|
|||||||
const [blockHash, setBlockHash] = useState<string>('');
|
const [blockHash, setBlockHash] = useState<string>('');
|
||||||
const [finalizedBlock, setFinalizedBlock] = useState<number>(0);
|
const [finalizedBlock, setFinalizedBlock] = useState<number>(0);
|
||||||
const [validatorCount, setValidatorCount] = useState<number>(0);
|
const [validatorCount, setValidatorCount] = useState<number>(0);
|
||||||
|
const [nominatorCount, setNominatorCount] = useState<number>(0);
|
||||||
const [peers, setPeers] = useState<number>(0);
|
const [peers, setPeers] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -17,6 +18,7 @@ export const NetworkStats: React.FC = () => {
|
|||||||
|
|
||||||
let unsubscribeNewHeads: () => void;
|
let unsubscribeNewHeads: () => void;
|
||||||
let unsubscribeFinalizedHeads: () => void;
|
let unsubscribeFinalizedHeads: () => void;
|
||||||
|
let intervalId: NodeJS.Timeout;
|
||||||
|
|
||||||
const subscribeToBlocks = async () => {
|
const subscribeToBlocks = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -31,13 +33,34 @@ export const NetworkStats: React.FC = () => {
|
|||||||
setFinalizedBlock(header.number.toNumber());
|
setFinalizedBlock(header.number.toNumber());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get validator count
|
// Update validator count, nominator count, and peer count every 3 seconds
|
||||||
const validators = await api.query.session.validators();
|
const updateNetworkStats = async () => {
|
||||||
setValidatorCount(validators.length);
|
try {
|
||||||
|
const validators = await api.query.session.validators();
|
||||||
|
const health = await api.rpc.system.health();
|
||||||
|
|
||||||
// Get peer count
|
// Count nominators
|
||||||
const health = await api.rpc.system.health();
|
let nominatorCount = 0;
|
||||||
setPeers(health.peers.toNumber());
|
try {
|
||||||
|
const nominators = await api.query.staking.nominators.entries();
|
||||||
|
nominatorCount = nominators.length;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Staking pallet not available, nominators = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidatorCount(validators.length);
|
||||||
|
setNominatorCount(nominatorCount);
|
||||||
|
setPeers(health.peers.toNumber());
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update network stats:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
await updateNetworkStats();
|
||||||
|
|
||||||
|
// Update every 3 seconds
|
||||||
|
intervalId = setInterval(updateNetworkStats, 3000);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to subscribe to blocks:', err);
|
console.error('Failed to subscribe to blocks:', err);
|
||||||
@@ -49,6 +72,7 @@ export const NetworkStats: React.FC = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
if (unsubscribeNewHeads) unsubscribeNewHeads();
|
if (unsubscribeNewHeads) unsubscribeNewHeads();
|
||||||
if (unsubscribeFinalizedHeads) unsubscribeFinalizedHeads();
|
if (unsubscribeFinalizedHeads) unsubscribeFinalizedHeads();
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [api, isApiReady]);
|
}, [api, isApiReady]);
|
||||||
|
|
||||||
@@ -85,7 +109,7 @@ export const NetworkStats: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
|
||||||
{/* Connection Status */}
|
{/* Connection Status */}
|
||||||
<Card className="bg-gray-900 border-gray-800">
|
<Card className="bg-gray-900 border-gray-800">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
@@ -155,7 +179,25 @@ export const NetworkStats: React.FC = () => {
|
|||||||
{validatorCount}
|
{validatorCount}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
Securing the network
|
Securing the network - LIVE
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Nominators */}
|
||||||
|
<Card className="bg-gray-900 border-gray-800">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
|
||||||
|
<Users className="w-4 h-4 text-cyan-500" />
|
||||||
|
Active Nominators
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{nominatorCount}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
Staking to validators
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -590,6 +590,8 @@ const PoolDashboard = () => {
|
|||||||
onClose={() => setIsRemoveLiquidityModalOpen(false)}
|
onClose={() => setIsRemoveLiquidityModalOpen(false)}
|
||||||
lpPosition={lpPosition}
|
lpPosition={lpPosition}
|
||||||
lpTokenId={poolData.lpTokenId}
|
lpTokenId={poolData.lpTokenId}
|
||||||
|
asset0={poolData.asset0}
|
||||||
|
asset1={poolData.asset1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { X, Minus, AlertCircle, Info } from 'lucide-react';
|
import { X, Minus, AlertCircle, Info } from 'lucide-react';
|
||||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
import { useWallet } from '@/contexts/WalletContext';
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||||
|
|
||||||
|
// Helper to get display name for tokens (users see HEZ not wHEZ, USDT not wUSDT)
|
||||||
|
const getDisplayTokenName = (assetId: number): string => {
|
||||||
|
if (assetId === ASSET_IDS.WHEZ || assetId === 0) return 'HEZ';
|
||||||
|
if (assetId === ASSET_IDS.PEZ || assetId === 1) return 'PEZ';
|
||||||
|
if (assetId === ASSET_IDS.WUSDT || assetId === 2) return 'USDT';
|
||||||
|
return getAssetSymbol(assetId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to get decimals for each asset
|
||||||
|
const getAssetDecimals = (assetId: number): number => {
|
||||||
|
if (assetId === ASSET_IDS.WUSDT) return 6; // wUSDT has 6 decimals
|
||||||
|
return 12; // wHEZ, PEZ have 12 decimals
|
||||||
|
};
|
||||||
|
|
||||||
interface RemoveLiquidityModalProps {
|
interface RemoveLiquidityModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -16,6 +31,8 @@ interface RemoveLiquidityModalProps {
|
|||||||
asset1Amount: number;
|
asset1Amount: number;
|
||||||
};
|
};
|
||||||
lpTokenId: number;
|
lpTokenId: number;
|
||||||
|
asset0: number; // First asset ID in the pool
|
||||||
|
asset1: number; // Second asset ID in the pool
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||||
@@ -23,6 +40,8 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
lpPosition,
|
lpPosition,
|
||||||
lpTokenId,
|
lpTokenId,
|
||||||
|
asset0,
|
||||||
|
asset1,
|
||||||
}) => {
|
}) => {
|
||||||
const { api, selectedAccount } = usePolkadot();
|
const { api, selectedAccount } = usePolkadot();
|
||||||
const { refreshBalances } = useWallet();
|
const { refreshBalances } = useWallet();
|
||||||
@@ -31,6 +50,87 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [minBalance0, setMinBalance0] = useState<number>(0);
|
||||||
|
const [minBalance1, setMinBalance1] = useState<number>(0);
|
||||||
|
const [maxRemovablePercentage, setMaxRemovablePercentage] = useState<number>(100);
|
||||||
|
|
||||||
|
// Fetch minimum balances for both assets
|
||||||
|
useEffect(() => {
|
||||||
|
if (!api || !isOpen) return;
|
||||||
|
|
||||||
|
const fetchMinBalances = async () => {
|
||||||
|
try {
|
||||||
|
console.log(`🔍 Fetching minBalances for pool: asset0=${asset0} (${getDisplayTokenName(asset0)}), asset1=${asset1} (${getDisplayTokenName(asset1)})`);
|
||||||
|
|
||||||
|
// For wHEZ (asset ID 0), we need to fetch from assets pallet
|
||||||
|
// For native HEZ, we would need existentialDeposit from balances
|
||||||
|
// But in our pools, we only use wHEZ, wUSDT, PEZ (all wrapped assets)
|
||||||
|
|
||||||
|
if (asset0 === ASSET_IDS.WHEZ || asset0 === 0) {
|
||||||
|
// wHEZ is an asset in the assets pallet
|
||||||
|
const assetDetails0 = await api.query.assets.asset(ASSET_IDS.WHEZ);
|
||||||
|
if (assetDetails0.isSome) {
|
||||||
|
const details0 = assetDetails0.unwrap().toJSON() as any;
|
||||||
|
const min0 = Number(details0.minBalance) / Math.pow(10, getAssetDecimals(asset0));
|
||||||
|
setMinBalance0(min0);
|
||||||
|
console.log(`📊 ${getDisplayTokenName(asset0)} minBalance: ${min0}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other assets (PEZ, wUSDT, etc.)
|
||||||
|
const assetDetails0 = await api.query.assets.asset(asset0);
|
||||||
|
if (assetDetails0.isSome) {
|
||||||
|
const details0 = assetDetails0.unwrap().toJSON() as any;
|
||||||
|
const min0 = Number(details0.minBalance) / Math.pow(10, getAssetDecimals(asset0));
|
||||||
|
setMinBalance0(min0);
|
||||||
|
console.log(`📊 ${getDisplayTokenName(asset0)} minBalance: ${min0}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset1 === ASSET_IDS.WHEZ || asset1 === 0) {
|
||||||
|
// wHEZ is an asset in the assets pallet
|
||||||
|
const assetDetails1 = await api.query.assets.asset(ASSET_IDS.WHEZ);
|
||||||
|
if (assetDetails1.isSome) {
|
||||||
|
const details1 = assetDetails1.unwrap().toJSON() as any;
|
||||||
|
const min1 = Number(details1.minBalance) / Math.pow(10, getAssetDecimals(asset1));
|
||||||
|
setMinBalance1(min1);
|
||||||
|
console.log(`📊 ${getDisplayTokenName(asset1)} minBalance: ${min1}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Other assets (PEZ, wUSDT, etc.)
|
||||||
|
const assetDetails1 = await api.query.assets.asset(asset1);
|
||||||
|
if (assetDetails1.isSome) {
|
||||||
|
const details1 = assetDetails1.unwrap().toJSON() as any;
|
||||||
|
const min1 = Number(details1.minBalance) / Math.pow(10, getAssetDecimals(asset1));
|
||||||
|
setMinBalance1(min1);
|
||||||
|
console.log(`📊 ${getDisplayTokenName(asset1)} minBalance: ${min1}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching minBalances:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMinBalances();
|
||||||
|
}, [api, isOpen, asset0, asset1]);
|
||||||
|
|
||||||
|
// Calculate maximum removable percentage based on minBalance requirements
|
||||||
|
useEffect(() => {
|
||||||
|
if (minBalance0 === 0 || minBalance1 === 0) return;
|
||||||
|
|
||||||
|
// Calculate what percentage would leave exactly minBalance
|
||||||
|
const maxPercent0 = ((lpPosition.asset0Amount - minBalance0) / lpPosition.asset0Amount) * 100;
|
||||||
|
const maxPercent1 = ((lpPosition.asset1Amount - minBalance1) / lpPosition.asset1Amount) * 100;
|
||||||
|
|
||||||
|
// Take the lower of the two (most restrictive)
|
||||||
|
const maxPercent = Math.min(maxPercent0, maxPercent1, 100);
|
||||||
|
|
||||||
|
// Round down to be safe
|
||||||
|
const safeMaxPercent = Math.floor(maxPercent * 10) / 10;
|
||||||
|
|
||||||
|
setMaxRemovablePercentage(safeMaxPercent > 0 ? safeMaxPercent : 99);
|
||||||
|
|
||||||
|
console.log(`🔒 Max removable: ${safeMaxPercent}% (asset0: ${maxPercent0.toFixed(2)}%, asset1: ${maxPercent1.toFixed(2)}%)`);
|
||||||
|
}, [minBalance0, minBalance1, lpPosition.asset0Amount, lpPosition.asset1Amount]);
|
||||||
|
|
||||||
const handleRemoveLiquidity = async () => {
|
const handleRemoveLiquidity = async () => {
|
||||||
if (!api || !selectedAccount) return;
|
if (!api || !selectedAccount) return;
|
||||||
@@ -42,32 +142,45 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
// Get the signer from the extension
|
// Get the signer from the extension
|
||||||
const injector = await web3FromAddress(selectedAccount.address);
|
const injector = await web3FromAddress(selectedAccount.address);
|
||||||
|
|
||||||
|
// Get decimals for each asset
|
||||||
|
const asset0Decimals = getAssetDecimals(asset0);
|
||||||
|
const asset1Decimals = getAssetDecimals(asset1);
|
||||||
|
|
||||||
// Calculate LP tokens to remove
|
// Calculate LP tokens to remove
|
||||||
const lpToRemove = (lpPosition.lpTokenBalance * percentage) / 100;
|
const lpToRemove = (lpPosition.lpTokenBalance * percentage) / 100;
|
||||||
const lpToRemoveBN = BigInt(Math.floor(lpToRemove * 1e12));
|
const lpToRemoveBN = BigInt(Math.floor(lpToRemove * 1e12));
|
||||||
|
|
||||||
// Calculate expected token amounts (with 95% slippage tolerance)
|
// Calculate expected token amounts (with 95% slippage tolerance)
|
||||||
const expectedWhezBN = BigInt(Math.floor((lpPosition.asset0Amount * percentage) / 100 * 1e12));
|
const expectedAsset0BN = BigInt(Math.floor((lpPosition.asset0Amount * percentage) / 100 * Math.pow(10, asset0Decimals)));
|
||||||
const expectedPezBN = BigInt(Math.floor((lpPosition.asset1Amount * percentage) / 100 * 1e12));
|
const expectedAsset1BN = BigInt(Math.floor((lpPosition.asset1Amount * percentage) / 100 * Math.pow(10, asset1Decimals)));
|
||||||
|
|
||||||
const minWhezBN = (expectedWhezBN * BigInt(95)) / BigInt(100);
|
const minAsset0BN = (expectedAsset0BN * BigInt(95)) / BigInt(100);
|
||||||
const minPezBN = (expectedPezBN * BigInt(95)) / BigInt(100);
|
const minAsset1BN = (expectedAsset1BN * BigInt(95)) / BigInt(100);
|
||||||
|
|
||||||
// Remove liquidity transaction
|
// Remove liquidity transaction
|
||||||
const removeLiquidityTx = api.tx.assetConversion.removeLiquidity(
|
const removeLiquidityTx = api.tx.assetConversion.removeLiquidity(
|
||||||
0, // asset1 (wHEZ)
|
asset0,
|
||||||
1, // asset2 (PEZ)
|
asset1,
|
||||||
lpToRemoveBN.toString(),
|
lpToRemoveBN.toString(),
|
||||||
minWhezBN.toString(),
|
minAsset0BN.toString(),
|
||||||
minPezBN.toString(),
|
minAsset1BN.toString(),
|
||||||
selectedAccount.address
|
selectedAccount.address
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unwrap wHEZ back to HEZ
|
// Check if we need to unwrap wHEZ back to HEZ
|
||||||
const unwrapTx = api.tx.tokenWrapper.unwrap(minWhezBN.toString());
|
const hasWHEZ = asset0 === ASSET_IDS.WHEZ || asset1 === ASSET_IDS.WHEZ;
|
||||||
|
let tx;
|
||||||
|
|
||||||
// Batch transactions
|
if (hasWHEZ) {
|
||||||
const tx = api.tx.utility.batchAll([removeLiquidityTx, unwrapTx]);
|
// Unwrap wHEZ back to HEZ
|
||||||
|
const whezAmount = asset0 === ASSET_IDS.WHEZ ? minAsset0BN : minAsset1BN;
|
||||||
|
const unwrapTx = api.tx.tokenWrapper.unwrap(whezAmount.toString());
|
||||||
|
// Batch transactions: removeLiquidity + unwrap
|
||||||
|
tx = api.tx.utility.batchAll([removeLiquidityTx, unwrapTx]);
|
||||||
|
} else {
|
||||||
|
// No unwrap needed for pools without wHEZ
|
||||||
|
tx = removeLiquidityTx;
|
||||||
|
}
|
||||||
|
|
||||||
await tx.signAndSend(
|
await tx.signAndSend(
|
||||||
selectedAccount.address,
|
selectedAccount.address,
|
||||||
@@ -108,8 +221,12 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const whezToReceive = (lpPosition.asset0Amount * percentage) / 100;
|
// Get display names for the assets
|
||||||
const pezToReceive = (lpPosition.asset1Amount * percentage) / 100;
|
const asset0Name = getDisplayTokenName(asset0);
|
||||||
|
const asset1Name = getDisplayTokenName(asset1);
|
||||||
|
|
||||||
|
const asset0ToReceive = (lpPosition.asset0Amount * percentage) / 100;
|
||||||
|
const asset1ToReceive = (lpPosition.asset1Amount * percentage) / 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
@@ -140,10 +257,20 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
<Alert className="mb-4 bg-blue-900/20 border-blue-500">
|
<Alert className="mb-4 bg-blue-900/20 border-blue-500">
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
<AlertDescription className="text-sm">
|
<AlertDescription className="text-sm">
|
||||||
Remove your liquidity to receive back your tokens. wHEZ will be automatically unwrapped to HEZ.
|
Remove your liquidity to receive back your tokens.{' '}
|
||||||
|
{(asset0 === ASSET_IDS.WHEZ || asset1 === ASSET_IDS.WHEZ) && 'wHEZ will be automatically unwrapped to HEZ.'}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
{maxRemovablePercentage < 100 && (
|
||||||
|
<Alert className="mb-4 bg-yellow-900/20 border-yellow-500">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription className="text-sm">
|
||||||
|
Maximum removable: {maxRemovablePercentage.toFixed(1)}% - Pool must maintain minimum balance of {minBalance0.toFixed(6)} {asset0Name} and {minBalance1.toFixed(6)} {asset1Name}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Percentage Selector */}
|
{/* Percentage Selector */}
|
||||||
<div>
|
<div>
|
||||||
@@ -155,28 +282,34 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="1"
|
min="1"
|
||||||
max="100"
|
max={maxRemovablePercentage}
|
||||||
value={percentage}
|
value={Math.min(percentage, maxRemovablePercentage)}
|
||||||
onChange={(e) => setPercentage(parseInt(e.target.value))}
|
onChange={(e) => setPercentage(parseInt(e.target.value))}
|
||||||
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between mt-2">
|
<div className="flex justify-between mt-2">
|
||||||
{[25, 50, 75, 100].map((p) => (
|
{[25, 50, 75, 100].map((p) => {
|
||||||
<button
|
const effectiveP = p === 100 ? Math.floor(maxRemovablePercentage) : p;
|
||||||
key={p}
|
const isDisabled = p > maxRemovablePercentage;
|
||||||
onClick={() => setPercentage(p)}
|
return (
|
||||||
className={`px-3 py-1 rounded text-sm ${
|
<button
|
||||||
percentage === p
|
key={p}
|
||||||
? 'bg-blue-600 text-white'
|
onClick={() => setPercentage(Math.min(effectiveP, maxRemovablePercentage))}
|
||||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
className={`px-3 py-1 rounded text-sm ${
|
||||||
}`}
|
percentage === effectiveP
|
||||||
disabled={isLoading}
|
? 'bg-blue-600 text-white'
|
||||||
>
|
: isDisabled
|
||||||
{p}%
|
? 'bg-gray-800 text-gray-500 cursor-not-allowed'
|
||||||
</button>
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
))}
|
}`}
|
||||||
|
disabled={isLoading || isDisabled}
|
||||||
|
>
|
||||||
|
{p === 100 ? 'MAX' : `${p}%`}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -186,9 +319,9 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
|
|
||||||
<div className="flex justify-between items-center p-3 bg-gray-900/50 rounded-lg">
|
<div className="flex justify-between items-center p-3 bg-gray-900/50 rounded-lg">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-400">HEZ</p>
|
<p className="text-xs text-gray-400">{asset0Name}</p>
|
||||||
<p className="text-xl font-bold text-white">
|
<p className="text-xl font-bold text-white">
|
||||||
{whezToReceive.toFixed(4)}
|
{asset0ToReceive.toFixed(4)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Minus className="w-5 h-5 text-gray-400" />
|
<Minus className="w-5 h-5 text-gray-400" />
|
||||||
@@ -196,9 +329,9 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
|
|
||||||
<div className="flex justify-between items-center p-3 bg-gray-900/50 rounded-lg">
|
<div className="flex justify-between items-center p-3 bg-gray-900/50 rounded-lg">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-gray-400">PEZ</p>
|
<p className="text-xs text-gray-400">{asset1Name}</p>
|
||||||
<p className="text-xl font-bold text-white">
|
<p className="text-xl font-bold text-white">
|
||||||
{pezToReceive.toFixed(4)}
|
{asset1ToReceive.toFixed(4)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Minus className="w-5 h-5 text-gray-400" />
|
<Minus className="w-5 h-5 text-gray-400" />
|
||||||
@@ -217,6 +350,18 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
{((lpPosition.lpTokenBalance * (100 - percentage)) / 100).toFixed(4)}
|
{((lpPosition.lpTokenBalance * (100 - percentage)) / 100).toFixed(4)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-between text-gray-300">
|
||||||
|
<span>Remaining {asset0Name}</span>
|
||||||
|
<span className={asset0ToReceive >= lpPosition.asset0Amount - minBalance0 ? 'text-yellow-400' : ''}>
|
||||||
|
{(lpPosition.asset0Amount - asset0ToReceive).toFixed(6)} (min: {minBalance0.toFixed(6)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-gray-300">
|
||||||
|
<span>Remaining {asset1Name}</span>
|
||||||
|
<span className={asset1ToReceive >= lpPosition.asset1Amount - minBalance1 ? 'text-yellow-400' : ''}>
|
||||||
|
{(lpPosition.asset1Amount - asset1ToReceive).toFixed(6)} (min: {minBalance1.toFixed(6)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between text-gray-300">
|
<div className="flex justify-between text-gray-300">
|
||||||
<span>Slippage Tolerance</span>
|
<span>Slippage Tolerance</span>
|
||||||
<span>5%</span>
|
<span>5%</span>
|
||||||
@@ -225,7 +370,7 @@ export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRemoveLiquidity}
|
onClick={handleRemoveLiquidity}
|
||||||
disabled={isLoading || percentage === 0}
|
disabled={isLoading || percentage === 0 || percentage > maxRemovablePercentage}
|
||||||
className="w-full bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 h-12"
|
className="w-full bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 h-12"
|
||||||
>
|
>
|
||||||
{isLoading ? 'Removing Liquidity...' : 'Remove Liquidity'}
|
{isLoading ? 'Removing Liquidity...' : 'Remove Liquidity'}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { LimitOrders } from './trading/LimitOrders';
|
|||||||
const AVAILABLE_TOKENS = [
|
const AVAILABLE_TOKENS = [
|
||||||
{ symbol: 'HEZ', emoji: '🟡', assetId: 0, name: 'HEZ', badge: true, displaySymbol: 'HEZ' },
|
{ symbol: 'HEZ', emoji: '🟡', assetId: 0, name: 'HEZ', badge: true, displaySymbol: 'HEZ' },
|
||||||
{ symbol: 'PEZ', emoji: '🟣', assetId: 1, name: 'PEZ', badge: true, displaySymbol: 'PEZ' },
|
{ symbol: 'PEZ', emoji: '🟣', assetId: 1, name: 'PEZ', badge: true, displaySymbol: 'PEZ' },
|
||||||
{ symbol: 'wUSDT', emoji: '💵', assetId: 2, name: 'USDT', badge: true, displaySymbol: 'USDT' },
|
{ symbol: 'USDT', emoji: '💵', assetId: 2, name: 'USDT', badge: true, displaySymbol: 'USDT' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const TokenSwap = () => {
|
const TokenSwap = () => {
|
||||||
@@ -178,7 +178,7 @@ const TokenSwap = () => {
|
|||||||
const getPoolAssetId = (token: string) => {
|
const getPoolAssetId = (token: string) => {
|
||||||
if (token === 'HEZ') return 0; // wHEZ
|
if (token === 'HEZ') return 0; // wHEZ
|
||||||
if (token === 'PEZ') return 1;
|
if (token === 'PEZ') return 1;
|
||||||
if (token === 'wUSDT') return 2;
|
if (token === 'USDT') return 2;
|
||||||
return ASSET_IDS[token as keyof typeof ASSET_IDS];
|
return ASSET_IDS[token as keyof typeof ASSET_IDS];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -426,8 +426,8 @@ const TokenSwap = () => {
|
|||||||
console.warn('Failed to parse swap path:', err);
|
console.warn('Failed to parse swap path:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 2 ? 'wUSDT' : `Asset${fromAssetId}`;
|
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 2 ? 'USDT' : `Asset${fromAssetId}`;
|
||||||
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 2 ? 'wUSDT' : `Asset${toAssetId}`;
|
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 2 ? 'USDT' : `Asset${toAssetId}`;
|
||||||
|
|
||||||
// Only show transactions from current user
|
// Only show transactions from current user
|
||||||
if (who.toString() === selectedAccount.address) {
|
if (who.toString() === selectedAccount.address) {
|
||||||
@@ -514,7 +514,7 @@ const TokenSwap = () => {
|
|||||||
try {
|
try {
|
||||||
// Get correct decimals for each token
|
// Get correct decimals for each token
|
||||||
const getTokenDecimals = (token: string) => {
|
const getTokenDecimals = (token: string) => {
|
||||||
if (token === 'wUSDT') return 6; // wUSDT has 6 decimals
|
if (token === 'USDT') return 6; // USDT has 6 decimals
|
||||||
return 12; // HEZ, wHEZ, PEZ all have 12 decimals
|
return 12; // HEZ, wHEZ, PEZ all have 12 decimals
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -577,7 +577,7 @@ const TokenSwap = () => {
|
|||||||
// HEZ → Any Asset: wrap(HEZ→wHEZ) then swap(wHEZ→Asset)
|
// HEZ → Any Asset: wrap(HEZ→wHEZ) then swap(wHEZ→Asset)
|
||||||
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
|
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
|
||||||
// Map token symbol to asset ID
|
// Map token symbol to asset ID
|
||||||
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'wUSDT' ? 2 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
|
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'USDT' ? 2 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
|
||||||
const swapPath = [0, toAssetId]; // wHEZ → target asset
|
const swapPath = [0, toAssetId]; // wHEZ → target asset
|
||||||
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
swapPath,
|
swapPath,
|
||||||
@@ -591,7 +591,7 @@ const TokenSwap = () => {
|
|||||||
} else if (toToken === 'HEZ') {
|
} else if (toToken === 'HEZ') {
|
||||||
// Any Asset → HEZ: swap(Asset→wHEZ) then unwrap(wHEZ→HEZ)
|
// Any Asset → HEZ: swap(Asset→wHEZ) then unwrap(wHEZ→HEZ)
|
||||||
// Map token symbol to asset ID
|
// Map token symbol to asset ID
|
||||||
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'wUSDT' ? 2 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'USDT' ? 2 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
||||||
const swapPath = [fromAssetId, 0]; // source asset → wHEZ
|
const swapPath = [fromAssetId, 0]; // source asset → wHEZ
|
||||||
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
swapPath,
|
swapPath,
|
||||||
@@ -604,10 +604,10 @@ const TokenSwap = () => {
|
|||||||
tx = api.tx.utility.batchAll([swapTx, unwrapTx]);
|
tx = api.tx.utility.batchAll([swapTx, unwrapTx]);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Direct swap between assets (PEZ ↔ wUSDT, etc.)
|
// Direct swap between assets (PEZ ↔ USDT, etc.)
|
||||||
// Map token symbols to asset IDs
|
// Map token symbols to asset IDs
|
||||||
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'wUSDT' ? 2 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
const fromAssetId = fromToken === 'PEZ' ? 1 : fromToken === 'USDT' ? 2 : ASSET_IDS[fromToken as keyof typeof ASSET_IDS];
|
||||||
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'wUSDT' ? 2 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
|
const toAssetId = toToken === 'PEZ' ? 1 : toToken === 'USDT' ? 2 : ASSET_IDS[toToken as keyof typeof ASSET_IDS];
|
||||||
const swapPath = [fromAssetId, toAssetId];
|
const swapPath = [fromAssetId, toAssetId];
|
||||||
|
|
||||||
tx = api.tx.assetConversion.swapExactTokensForTokens(
|
tx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
@@ -722,8 +722,8 @@ const TokenSwap = () => {
|
|||||||
console.warn('Failed to parse swap path in refresh:', err);
|
console.warn('Failed to parse swap path in refresh:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : `Asset${fromAssetId}`;
|
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 2 ? 'USDT' : `Asset${fromAssetId}`;
|
||||||
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : `Asset${toAssetId}`;
|
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 2 ? 'USDT' : `Asset${toAssetId}`;
|
||||||
|
|
||||||
if (who.toString() === selectedAccount.address) {
|
if (who.toString() === selectedAccount.address) {
|
||||||
transactions.push({
|
transactions.push({
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
|
import { X, Plus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { PoolInfo } from '@/types/dex';
|
||||||
|
import { parseTokenInput, formatTokenBalance, quote } from '@/utils/dex';
|
||||||
|
|
||||||
|
interface AddLiquidityModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
pool: PoolInfo | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error';
|
||||||
|
|
||||||
|
export const AddLiquidityModal: React.FC<AddLiquidityModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
pool,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const { api, isApiReady } = usePolkadot();
|
||||||
|
const { account, signer } = useWallet();
|
||||||
|
|
||||||
|
const [amount1Input, setAmount1Input] = useState('');
|
||||||
|
const [amount2Input, setAmount2Input] = useState('');
|
||||||
|
const [slippage, setSlippage] = useState(1); // 1% default
|
||||||
|
|
||||||
|
const [balance1, setBalance1] = useState<string>('0');
|
||||||
|
const [balance2, setBalance2] = useState<string>('0');
|
||||||
|
|
||||||
|
const [txStatus, setTxStatus] = useState<TransactionStatus>('idle');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
|
// Reset form when modal closes or pool changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !pool) {
|
||||||
|
setAmount1Input('');
|
||||||
|
setAmount2Input('');
|
||||||
|
setTxStatus('idle');
|
||||||
|
setErrorMessage('');
|
||||||
|
}
|
||||||
|
}, [isOpen, pool]);
|
||||||
|
|
||||||
|
// Fetch balances
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBalances = async () => {
|
||||||
|
if (!api || !isApiReady || !account || !pool) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const balance1Data = await api.query.assets.account(pool.asset1, account);
|
||||||
|
const balance2Data = await api.query.assets.account(pool.asset2, account);
|
||||||
|
|
||||||
|
setBalance1(balance1Data.isSome ? balance1Data.unwrap().balance.toString() : '0');
|
||||||
|
setBalance2(balance2Data.isSome ? balance2Data.unwrap().balance.toString() : '0');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch balances:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBalances();
|
||||||
|
}, [api, isApiReady, account, pool]);
|
||||||
|
|
||||||
|
// Auto-calculate amount2 when amount1 changes
|
||||||
|
const handleAmount1Change = (value: string) => {
|
||||||
|
setAmount1Input(value);
|
||||||
|
|
||||||
|
if (!pool || !value || parseFloat(value) === 0) {
|
||||||
|
setAmount2Input('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const amount1Raw = parseTokenInput(value, pool.asset1Decimals);
|
||||||
|
const amount2Raw = quote(amount1Raw, pool.reserve2, pool.reserve1);
|
||||||
|
const amount2Display = formatTokenBalance(amount2Raw, pool.asset2Decimals, 6);
|
||||||
|
setAmount2Input(amount2Display);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to calculate amount2:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-calculate amount1 when amount2 changes
|
||||||
|
const handleAmount2Change = (value: string) => {
|
||||||
|
setAmount2Input(value);
|
||||||
|
|
||||||
|
if (!pool || !value || parseFloat(value) === 0) {
|
||||||
|
setAmount1Input('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const amount2Raw = parseTokenInput(value, pool.asset2Decimals);
|
||||||
|
const amount1Raw = quote(amount2Raw, pool.reserve1, pool.reserve2);
|
||||||
|
const amount1Display = formatTokenBalance(amount1Raw, pool.asset1Decimals, 6);
|
||||||
|
setAmount1Input(amount1Display);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to calculate amount1:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateInputs = (): string | null => {
|
||||||
|
if (!pool) return 'No pool selected';
|
||||||
|
if (!amount1Input || !amount2Input) return 'Please enter amounts';
|
||||||
|
|
||||||
|
const amount1Raw = parseTokenInput(amount1Input, pool.asset1Decimals);
|
||||||
|
const amount2Raw = parseTokenInput(amount2Input, pool.asset2Decimals);
|
||||||
|
|
||||||
|
if (BigInt(amount1Raw) <= BigInt(0) || BigInt(amount2Raw) <= BigInt(0)) {
|
||||||
|
return 'Amounts must be greater than zero';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BigInt(amount1Raw) > BigInt(balance1)) {
|
||||||
|
return `Insufficient ${pool.asset1Symbol} balance`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BigInt(amount2Raw) > BigInt(balance2)) {
|
||||||
|
return `Insufficient ${pool.asset2Symbol} balance`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddLiquidity = async () => {
|
||||||
|
if (!api || !isApiReady || !signer || !account || !pool) {
|
||||||
|
setErrorMessage('Wallet not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationError = validateInputs();
|
||||||
|
if (validationError) {
|
||||||
|
setErrorMessage(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount1Raw = parseTokenInput(amount1Input, pool.asset1Decimals);
|
||||||
|
const amount2Raw = parseTokenInput(amount2Input, pool.asset2Decimals);
|
||||||
|
|
||||||
|
// Calculate minimum amounts with slippage tolerance
|
||||||
|
const minAmount1 = (BigInt(amount1Raw) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
||||||
|
const minAmount2 = (BigInt(amount2Raw) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setTxStatus('signing');
|
||||||
|
setErrorMessage('');
|
||||||
|
|
||||||
|
const tx = api.tx.assetConversion.addLiquidity(
|
||||||
|
pool.asset1,
|
||||||
|
pool.asset2,
|
||||||
|
amount1Raw,
|
||||||
|
amount2Raw,
|
||||||
|
minAmount1.toString(),
|
||||||
|
minAmount2.toString(),
|
||||||
|
account
|
||||||
|
);
|
||||||
|
|
||||||
|
setTxStatus('submitting');
|
||||||
|
|
||||||
|
await tx.signAndSend(
|
||||||
|
account,
|
||||||
|
{ signer },
|
||||||
|
({ status, dispatchError }) => {
|
||||||
|
if (status.isInBlock) {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||||
|
setErrorMessage(`${decoded.section}.${decoded.name}: ${decoded.docs}`);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(dispatchError.toString());
|
||||||
|
}
|
||||||
|
setTxStatus('error');
|
||||||
|
} else {
|
||||||
|
setTxStatus('success');
|
||||||
|
setTimeout(() => {
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Add liquidity failed:', error);
|
||||||
|
setErrorMessage(error.message || 'Transaction failed');
|
||||||
|
setTxStatus('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen || !pool) return null;
|
||||||
|
|
||||||
|
const shareOfPool =
|
||||||
|
amount1Input && parseFloat(amount1Input) > 0
|
||||||
|
? (
|
||||||
|
(parseFloat(
|
||||||
|
formatTokenBalance(
|
||||||
|
parseTokenInput(amount1Input, pool.asset1Decimals),
|
||||||
|
pool.asset1Decimals,
|
||||||
|
6
|
||||||
|
)
|
||||||
|
) /
|
||||||
|
(parseFloat(formatTokenBalance(pool.reserve1, pool.asset1Decimals, 6)) +
|
||||||
|
parseFloat(
|
||||||
|
formatTokenBalance(
|
||||||
|
parseTokenInput(amount1Input, pool.asset1Decimals),
|
||||||
|
pool.asset1Decimals,
|
||||||
|
6
|
||||||
|
)
|
||||||
|
))) *
|
||||||
|
100
|
||||||
|
).toFixed(4)
|
||||||
|
: '0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
|
<Card className="bg-gray-900 border-gray-800 max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<CardHeader className="border-b border-gray-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl font-bold text-white">
|
||||||
|
Add Liquidity
|
||||||
|
</CardTitle>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400 mt-2">
|
||||||
|
{pool.asset1Symbol} / {pool.asset2Symbol} Pool
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6 pt-6">
|
||||||
|
{/* Info Banner */}
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||||
|
<Info className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-blue-400">
|
||||||
|
Add liquidity in proportion to the pool's current ratio. You'll receive LP tokens representing your share.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Token 1 Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm text-gray-400">{pool.asset1Symbol}</label>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
Balance: {formatTokenBalance(balance1, pool.asset1Decimals, 4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={amount1Input}
|
||||||
|
onChange={(e) => handleAmount1Change(e.target.value)}
|
||||||
|
placeholder="0.0"
|
||||||
|
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-lg placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleAmount1Change(formatTokenBalance(balance1, pool.asset1Decimals, 6))
|
||||||
|
}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Plus Icon */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-800 border border-gray-700 flex items-center justify-center">
|
||||||
|
<Plus className="w-5 h-5 text-green-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Token 2 Input */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm text-gray-400">{pool.asset2Symbol}</label>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
Balance: {formatTokenBalance(balance2, pool.asset2Decimals, 4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={amount2Input}
|
||||||
|
onChange={(e) => handleAmount2Change(e.target.value)}
|
||||||
|
placeholder="0.0"
|
||||||
|
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-lg placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleAmount2Change(formatTokenBalance(balance2, pool.asset2Decimals, 6))
|
||||||
|
}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slippage Tolerance */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Slippage Tolerance</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[0.5, 1, 2].map((value) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
onClick={() => setSlippage(value)}
|
||||||
|
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
slippage === value
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
{value}%
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pool Share Preview */}
|
||||||
|
{amount1Input && amount2Input && (
|
||||||
|
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Share of Pool</span>
|
||||||
|
<span className="text-white font-mono">{shareOfPool}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Exchange Rate</span>
|
||||||
|
<span className="text-cyan-400 font-mono">
|
||||||
|
1 {pool.asset1Symbol} ={' '}
|
||||||
|
{(
|
||||||
|
parseFloat(formatTokenBalance(pool.reserve2, pool.asset2Decimals, 6)) /
|
||||||
|
parseFloat(formatTokenBalance(pool.reserve1, pool.asset1Decimals, 6))
|
||||||
|
).toFixed(6)}{' '}
|
||||||
|
{pool.asset2Symbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-red-400">{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-green-400">
|
||||||
|
Liquidity added successfully!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 px-6 py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-lg transition-colors border border-gray-700"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleAddLiquidity}
|
||||||
|
className="flex-1 px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium flex items-center justify-center gap-2"
|
||||||
|
disabled={
|
||||||
|
txStatus === 'signing' ||
|
||||||
|
txStatus === 'submitting' ||
|
||||||
|
txStatus === 'success'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{txStatus === 'signing' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Signing...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'submitting' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Adding...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'idle' && 'Add Liquidity'}
|
||||||
|
{txStatus === 'error' && 'Retry'}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
Success
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
|
import { X, Plus, AlertCircle, Loader2, CheckCircle } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { KNOWN_TOKENS } from '@/types/dex';
|
||||||
|
import { parseTokenInput, formatTokenBalance } from '@/utils/dex';
|
||||||
|
|
||||||
|
interface CreatePoolModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error';
|
||||||
|
|
||||||
|
export const CreatePoolModal: React.FC<CreatePoolModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const { api, isApiReady } = usePolkadot();
|
||||||
|
const { account, signer } = useWallet();
|
||||||
|
|
||||||
|
const [asset1Id, setAsset1Id] = useState<number | null>(null);
|
||||||
|
const [asset2Id, setAsset2Id] = useState<number | null>(null);
|
||||||
|
const [amount1Input, setAmount1Input] = useState('');
|
||||||
|
const [amount2Input, setAmount2Input] = useState('');
|
||||||
|
|
||||||
|
const [balance1, setBalance1] = useState<string>('0');
|
||||||
|
const [balance2, setBalance2] = useState<string>('0');
|
||||||
|
|
||||||
|
const [txStatus, setTxStatus] = useState<TransactionStatus>('idle');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
|
// Available tokens
|
||||||
|
const availableTokens = Object.values(KNOWN_TOKENS);
|
||||||
|
|
||||||
|
// Reset form when modal closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setAsset1Id(null);
|
||||||
|
setAsset2Id(null);
|
||||||
|
setAmount1Input('');
|
||||||
|
setAmount2Input('');
|
||||||
|
setTxStatus('idle');
|
||||||
|
setErrorMessage('');
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Fetch balances when assets selected
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBalances = async () => {
|
||||||
|
if (!api || !isApiReady || !account || asset1Id === null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Fetching balance for asset', asset1Id, 'account', account);
|
||||||
|
const balance1Data = await api.query.assets.account(asset1Id, account);
|
||||||
|
if (balance1Data.isSome) {
|
||||||
|
const balance = balance1Data.unwrap().balance.toString();
|
||||||
|
console.log('✅ Balance found for asset', asset1Id, ':', balance);
|
||||||
|
setBalance1(balance);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ No balance found for asset', asset1Id);
|
||||||
|
setBalance1('0');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to fetch balance 1:', error);
|
||||||
|
setBalance1('0');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBalances();
|
||||||
|
}, [api, isApiReady, account, asset1Id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBalances = async () => {
|
||||||
|
if (!api || !isApiReady || !account || asset2Id === null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Fetching balance for asset', asset2Id, 'account', account);
|
||||||
|
const balance2Data = await api.query.assets.account(asset2Id, account);
|
||||||
|
if (balance2Data.isSome) {
|
||||||
|
const balance = balance2Data.unwrap().balance.toString();
|
||||||
|
console.log('✅ Balance found for asset', asset2Id, ':', balance);
|
||||||
|
setBalance2(balance);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ No balance found for asset', asset2Id);
|
||||||
|
setBalance2('0');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to fetch balance 2:', error);
|
||||||
|
setBalance2('0');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBalances();
|
||||||
|
}, [api, isApiReady, account, asset2Id]);
|
||||||
|
|
||||||
|
const validateInputs = (): string | null => {
|
||||||
|
if (asset1Id === null || asset2Id === null) {
|
||||||
|
return 'Please select both tokens';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset1Id === asset2Id) {
|
||||||
|
return 'Cannot create pool with same token';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amount1Input || !amount2Input) {
|
||||||
|
return 'Please enter amounts for both tokens';
|
||||||
|
}
|
||||||
|
|
||||||
|
const token1 = KNOWN_TOKENS[asset1Id];
|
||||||
|
const token2 = KNOWN_TOKENS[asset2Id];
|
||||||
|
|
||||||
|
if (!token1 || !token2) {
|
||||||
|
return 'Invalid token selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount1Raw = parseTokenInput(amount1Input, token1.decimals);
|
||||||
|
const amount2Raw = parseTokenInput(amount2Input, token2.decimals);
|
||||||
|
|
||||||
|
console.log('💰 Validation check:', {
|
||||||
|
token1: token1.symbol,
|
||||||
|
amount1Input,
|
||||||
|
amount1Raw,
|
||||||
|
balance1,
|
||||||
|
hasEnough1: BigInt(amount1Raw) <= BigInt(balance1),
|
||||||
|
token2: token2.symbol,
|
||||||
|
amount2Input,
|
||||||
|
amount2Raw,
|
||||||
|
balance2,
|
||||||
|
hasEnough2: BigInt(amount2Raw) <= BigInt(balance2),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (BigInt(amount1Raw) <= BigInt(0) || BigInt(amount2Raw) <= BigInt(0)) {
|
||||||
|
return 'Amounts must be greater than zero';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BigInt(amount1Raw) > BigInt(balance1)) {
|
||||||
|
return `Insufficient ${token1.symbol} balance`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BigInt(amount2Raw) > BigInt(balance2)) {
|
||||||
|
return `Insufficient ${token2.symbol} balance`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreatePool = async () => {
|
||||||
|
if (!api || !isApiReady || !signer || !account) {
|
||||||
|
setErrorMessage('Wallet not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationError = validateInputs();
|
||||||
|
if (validationError) {
|
||||||
|
setErrorMessage(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token1 = KNOWN_TOKENS[asset1Id!];
|
||||||
|
const token2 = KNOWN_TOKENS[asset2Id!];
|
||||||
|
const amount1Raw = parseTokenInput(amount1Input, token1.decimals);
|
||||||
|
const amount2Raw = parseTokenInput(amount2Input, token2.decimals);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setTxStatus('signing');
|
||||||
|
setErrorMessage('');
|
||||||
|
|
||||||
|
// Create pool extrinsic
|
||||||
|
const createPoolTx = api.tx.assetConversion.createPool(asset1Id, asset2Id);
|
||||||
|
|
||||||
|
// Add liquidity extrinsic
|
||||||
|
const addLiquidityTx = api.tx.assetConversion.addLiquidity(
|
||||||
|
asset1Id,
|
||||||
|
asset2Id,
|
||||||
|
amount1Raw,
|
||||||
|
amount2Raw,
|
||||||
|
amount1Raw, // min amount1
|
||||||
|
amount2Raw, // min amount2
|
||||||
|
account
|
||||||
|
);
|
||||||
|
|
||||||
|
// Batch transactions
|
||||||
|
const batchTx = api.tx.utility.batchAll([createPoolTx, addLiquidityTx]);
|
||||||
|
|
||||||
|
setTxStatus('submitting');
|
||||||
|
|
||||||
|
await batchTx.signAndSend(
|
||||||
|
account,
|
||||||
|
{ signer },
|
||||||
|
({ status, dispatchError }) => {
|
||||||
|
if (status.isInBlock) {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||||
|
setErrorMessage(`${decoded.section}.${decoded.name}: ${decoded.docs}`);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(dispatchError.toString());
|
||||||
|
}
|
||||||
|
setTxStatus('error');
|
||||||
|
} else {
|
||||||
|
setTxStatus('success');
|
||||||
|
setTimeout(() => {
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Pool creation failed:', error);
|
||||||
|
setErrorMessage(error.message || 'Transaction failed');
|
||||||
|
setTxStatus('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const token1 = asset1Id !== null ? KNOWN_TOKENS[asset1Id] : null;
|
||||||
|
const token2 = asset2Id !== null ? KNOWN_TOKENS[asset2Id] : null;
|
||||||
|
|
||||||
|
const exchangeRate =
|
||||||
|
amount1Input && amount2Input && parseFloat(amount1Input) > 0
|
||||||
|
? (parseFloat(amount2Input) / parseFloat(amount1Input)).toFixed(6)
|
||||||
|
: '0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
|
<Card className="bg-gray-900 border-gray-800 max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<CardHeader className="border-b border-gray-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl font-bold text-white">
|
||||||
|
Create New Pool
|
||||||
|
</CardTitle>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-green-600/20 text-green-400 border-green-600/30 w-fit mt-2">
|
||||||
|
Founder Only
|
||||||
|
</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6 pt-6">
|
||||||
|
{/* Token 1 Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Token 1</label>
|
||||||
|
<select
|
||||||
|
value={asset1Id ?? ''}
|
||||||
|
onChange={(e) => setAsset1Id(Number(e.target.value))}
|
||||||
|
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
<option value="">Select token...</option>
|
||||||
|
{availableTokens.map((token) => (
|
||||||
|
<option key={token.id} value={token.id}>
|
||||||
|
{token.symbol} - {token.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{token1 && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Balance: {formatTokenBalance(balance1, token1.decimals, 4)} {token1.symbol}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Amount 1 Input */}
|
||||||
|
{token1 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">
|
||||||
|
Amount of {token1.symbol}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={amount1Input}
|
||||||
|
onChange={(e) => setAmount1Input(e.target.value)}
|
||||||
|
placeholder="0.0"
|
||||||
|
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Plus Icon */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-800 border border-gray-700 flex items-center justify-center">
|
||||||
|
<Plus className="w-5 h-5 text-green-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Token 2 Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Token 2</label>
|
||||||
|
<select
|
||||||
|
value={asset2Id ?? ''}
|
||||||
|
onChange={(e) => setAsset2Id(Number(e.target.value))}
|
||||||
|
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
<option value="">Select token...</option>
|
||||||
|
{availableTokens.map((token) => (
|
||||||
|
<option key={token.id} value={token.id} disabled={token.id === asset1Id}>
|
||||||
|
{token.symbol} - {token.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{token2 && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Balance: {formatTokenBalance(balance2, token2.decimals, 4)} {token2.symbol}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Amount 2 Input */}
|
||||||
|
{token2 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">
|
||||||
|
Amount of {token2.symbol}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={amount2Input}
|
||||||
|
onChange={(e) => setAmount2Input(e.target.value)}
|
||||||
|
placeholder="0.0"
|
||||||
|
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Exchange Rate Preview */}
|
||||||
|
{token1 && token2 && amount1Input && amount2Input && (
|
||||||
|
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||||
|
<div className="text-sm text-gray-400 mb-2">Initial Exchange Rate</div>
|
||||||
|
<div className="text-white font-mono">
|
||||||
|
1 {token1.symbol} = {exchangeRate} {token2.symbol}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-red-400">{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-green-400">
|
||||||
|
Pool created successfully!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 px-6 py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-lg transition-colors border border-gray-700"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCreatePool}
|
||||||
|
className="flex-1 px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium flex items-center justify-center gap-2"
|
||||||
|
disabled={
|
||||||
|
txStatus === 'signing' ||
|
||||||
|
txStatus === 'submitting' ||
|
||||||
|
txStatus === 'success'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{txStatus === 'signing' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Signing...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'submitting' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'idle' && 'Create Pool'}
|
||||||
|
{txStatus === 'error' && 'Retry'}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
Success
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import TokenSwap from '@/components/TokenSwap';
|
||||||
|
import PoolDashboard from '@/components/PoolDashboard';
|
||||||
|
import { CreatePoolModal } from './CreatePoolModal';
|
||||||
|
import { InitializeHezPoolModal } from './InitializeHezPoolModal';
|
||||||
|
import { ArrowRightLeft, Droplet, Settings } from 'lucide-react';
|
||||||
|
import { isFounderWallet } from '@/utils/auth';
|
||||||
|
|
||||||
|
export const DEXDashboard: React.FC = () => {
|
||||||
|
const { account } = useWallet();
|
||||||
|
const [activeTab, setActiveTab] = useState('swap');
|
||||||
|
|
||||||
|
// Admin modal states
|
||||||
|
const [showCreatePoolModal, setShowCreatePoolModal] = useState(false);
|
||||||
|
const [showInitializeHezPoolModal, setShowInitializeHezPoolModal] = useState(false);
|
||||||
|
|
||||||
|
const isFounder = account ? isFounderWallet(account) : false;
|
||||||
|
|
||||||
|
const handleCreatePool = () => {
|
||||||
|
setShowCreatePoolModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setShowCreatePoolModal(false);
|
||||||
|
setShowInitializeHezPoolModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuccess = async () => {
|
||||||
|
// Pool modals will refresh their own data
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-950 text-white">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gradient-to-r from-green-900/30 via-yellow-900/30 to-red-900/30 border-b border-gray-800 py-8">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 bg-clip-text text-transparent">
|
||||||
|
Pezkuwi DEX
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-400 text-lg">
|
||||||
|
Decentralized exchange for trading tokens on PezkuwiChain
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Wallet status */}
|
||||||
|
{account && (
|
||||||
|
<div className="mt-4 flex items-center gap-4">
|
||||||
|
<div className="px-4 py-2 bg-gray-900/80 rounded-lg border border-gray-800">
|
||||||
|
<span className="text-xs text-gray-400">Connected: </span>
|
||||||
|
<span className="text-sm font-mono text-white">
|
||||||
|
{account.slice(0, 6)}...{account.slice(-4)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isFounder && (
|
||||||
|
<div className="px-4 py-2 bg-green-600/20 border border-green-600/30 rounded-lg">
|
||||||
|
<span className="text-xs text-green-400 font-semibold">
|
||||||
|
FOUNDER ACCESS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||||
|
{!account ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="mb-4 text-gray-400 text-lg">
|
||||||
|
Please connect your Polkadot wallet to use the DEX
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
|
<TabsList className={`grid w-full ${isFounder ? 'grid-cols-3' : 'grid-cols-2'} gap-2 bg-gray-900/50 p-1 rounded-lg mb-8`}>
|
||||||
|
<TabsTrigger value="swap" className="flex items-center gap-2">
|
||||||
|
<ArrowRightLeft className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Swap</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="pools" className="flex items-center gap-2">
|
||||||
|
<Droplet className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Pools</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
{isFounder && (
|
||||||
|
<TabsTrigger value="admin" className="flex items-center gap-2">
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Admin</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="swap" className="mt-6">
|
||||||
|
<TokenSwap />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="pools" className="mt-6">
|
||||||
|
<PoolDashboard />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{isFounder && (
|
||||||
|
<TabsContent value="admin" className="mt-6">
|
||||||
|
<div className="max-w-2xl mx-auto space-y-6">
|
||||||
|
<div className="p-6 bg-gray-900 border border-blue-900/30 rounded-lg">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-2">Token Wrapping</h3>
|
||||||
|
<p className="text-gray-400 mb-6">
|
||||||
|
Convert native HEZ to wrapped wHEZ for use in DEX pools
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowInitializeHezPoolModal(true)}
|
||||||
|
className="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Wrap HEZ to wHEZ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-900 border border-gray-800 rounded-lg">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-2">Pool Management</h3>
|
||||||
|
<p className="text-gray-400 mb-6">
|
||||||
|
Create new liquidity pools for token pairs on PezkuwiChain
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleCreatePool}
|
||||||
|
className="w-full px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Create New Pool
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-900 border border-gray-800 rounded-lg">
|
||||||
|
<h3 className="text-xl font-bold text-white mb-2">Pool Statistics</h3>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
View detailed pool statistics in the Pools tab
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Modals */}
|
||||||
|
<CreatePoolModal
|
||||||
|
isOpen={showCreatePoolModal}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InitializeHezPoolModal
|
||||||
|
isOpen={showInitializeHezPoolModal}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
|
import { X, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
interface InitializeHezPoolModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error';
|
||||||
|
|
||||||
|
export const InitializeHezPoolModal: React.FC<InitializeHezPoolModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const { api, isApiReady } = usePolkadot();
|
||||||
|
const { account, signer } = useWallet();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [hezAmount, setHezAmount] = useState('100000');
|
||||||
|
|
||||||
|
const [hezBalance, setHezBalance] = useState<string>('0');
|
||||||
|
const [whezBalance, setWhezBalance] = useState<string>('0');
|
||||||
|
|
||||||
|
const [txStatus, setTxStatus] = useState<TransactionStatus>('idle');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
|
// Reset form when modal closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setHezAmount('100000');
|
||||||
|
setTxStatus('idle');
|
||||||
|
setErrorMessage('');
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Fetch balances
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBalances = async () => {
|
||||||
|
if (!api || !isApiReady || !account) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// HEZ balance (native)
|
||||||
|
const balance = await api.query.system.account(account);
|
||||||
|
const freeBalance = balance.data.free.toString();
|
||||||
|
setHezBalance(freeBalance);
|
||||||
|
|
||||||
|
// wHEZ balance (asset 0)
|
||||||
|
const whezData = await api.query.assets.account(0, account);
|
||||||
|
setWhezBalance(whezData.isSome ? whezData.unwrap().balance.toString() : '0');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch balances:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBalances();
|
||||||
|
}, [api, isApiReady, account]);
|
||||||
|
|
||||||
|
const handleWrap = async () => {
|
||||||
|
if (!api || !isApiReady || !signer || !account) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Please connect your wallet',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hezAmountRaw = BigInt(parseFloat(hezAmount) * 10 ** 12);
|
||||||
|
|
||||||
|
if (hezAmountRaw <= BigInt(0)) {
|
||||||
|
setErrorMessage('Amount must be greater than zero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hezAmountRaw > BigInt(hezBalance)) {
|
||||||
|
setErrorMessage('Insufficient HEZ balance');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTxStatus('signing');
|
||||||
|
setErrorMessage('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔄 Wrapping HEZ to wHEZ...', {
|
||||||
|
hezAmount,
|
||||||
|
hezAmountRaw: hezAmountRaw.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapTx = api.tx.tokenWrapper.wrap(hezAmountRaw.toString());
|
||||||
|
|
||||||
|
setTxStatus('submitting');
|
||||||
|
|
||||||
|
await wrapTx.signAndSend(
|
||||||
|
account,
|
||||||
|
{ signer },
|
||||||
|
({ status, dispatchError, events }) => {
|
||||||
|
console.log('📦 Transaction status:', status.type);
|
||||||
|
|
||||||
|
if (status.isInBlock) {
|
||||||
|
console.log('✅ In block:', status.asInBlock.toHex());
|
||||||
|
|
||||||
|
if (dispatchError) {
|
||||||
|
let errorMsg = '';
|
||||||
|
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||||
|
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||||
|
console.error('❌ Module error:', errorMsg);
|
||||||
|
} else {
|
||||||
|
errorMsg = dispatchError.toString();
|
||||||
|
console.error('❌ Dispatch error:', errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage(errorMsg);
|
||||||
|
setTxStatus('error');
|
||||||
|
toast({
|
||||||
|
title: 'Transaction Failed',
|
||||||
|
description: errorMsg,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('✅ Wrap successful!');
|
||||||
|
console.log('📋 Events:', events.map(e => e.event.method).join(', '));
|
||||||
|
setTxStatus('success');
|
||||||
|
toast({
|
||||||
|
title: 'Success!',
|
||||||
|
description: `Successfully wrapped ${hezAmount} HEZ to wHEZ`,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Wrap failed:', error);
|
||||||
|
setErrorMessage(error.message || 'Transaction failed');
|
||||||
|
setTxStatus('error');
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: error.message || 'Wrap failed',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const hezBalanceDisplay = (parseFloat(hezBalance) / 10 ** 12).toFixed(4);
|
||||||
|
const whezBalanceDisplay = (parseFloat(whezBalance) / 10 ** 12).toFixed(4);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
|
<Card className="bg-gray-900 border-gray-800 max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<CardHeader className="border-b border-gray-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl font-bold text-white">
|
||||||
|
Wrap HEZ to wHEZ
|
||||||
|
</CardTitle>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-blue-600/20 text-blue-400 border-blue-600/30 w-fit mt-2">
|
||||||
|
Admin Only - Token Wrapping
|
||||||
|
</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6 pt-6">
|
||||||
|
{/* Info Banner */}
|
||||||
|
<Alert className="bg-blue-500/10 border-blue-500/30">
|
||||||
|
<Info className="h-4 w-4 text-blue-400" />
|
||||||
|
<AlertDescription className="text-blue-300 text-sm">
|
||||||
|
Convert native HEZ tokens to wHEZ (wrapped HEZ) tokens for use in DEX pools.
|
||||||
|
Ratio is always 1:1.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* HEZ Amount */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm text-gray-400">HEZ Amount</label>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
Balance: {hezBalanceDisplay} HEZ
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={hezAmount}
|
||||||
|
onChange={(e) => setHezAmount(e.target.value)}
|
||||||
|
placeholder="1000"
|
||||||
|
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-lg"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setHezAmount((parseFloat(hezBalance) / 10 ** 12).toFixed(4))}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
💡 You will receive {hezAmount} wHEZ (1:1 ratio)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current wHEZ Balance */}
|
||||||
|
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Current wHEZ Balance</div>
|
||||||
|
<div className="text-2xl font-bold text-cyan-400 font-mono">
|
||||||
|
{whezBalanceDisplay} wHEZ
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{errorMessage && (
|
||||||
|
<Alert className="bg-red-500/10 border-red-500/30">
|
||||||
|
<AlertCircle className="h-4 w-4 text-red-400" />
|
||||||
|
<AlertDescription className="text-red-300 text-sm">
|
||||||
|
{errorMessage}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<Alert className="bg-green-500/10 border-green-500/30">
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||||
|
<AlertDescription className="text-green-300 text-sm">
|
||||||
|
Successfully wrapped {hezAmount} HEZ to wHEZ!
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1 border-gray-700 hover:bg-gray-800"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleWrap}
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700"
|
||||||
|
disabled={
|
||||||
|
txStatus === 'signing' ||
|
||||||
|
txStatus === 'submitting' ||
|
||||||
|
txStatus === 'success'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{txStatus === 'signing' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||||
|
Signing...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'submitting' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||||
|
Wrapping...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'idle' && 'Wrap HEZ'}
|
||||||
|
{txStatus === 'error' && 'Retry'}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
Success
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { TrendingUp, Droplet, BarChart3, Search, Plus } from 'lucide-react';
|
||||||
|
import { PoolInfo } from '@/types/dex';
|
||||||
|
import { fetchPools, formatTokenBalance } from '@/utils/dex';
|
||||||
|
import { isFounderWallet } from '@/utils/auth';
|
||||||
|
|
||||||
|
interface PoolBrowserProps {
|
||||||
|
onAddLiquidity?: (pool: PoolInfo) => void;
|
||||||
|
onRemoveLiquidity?: (pool: PoolInfo) => void;
|
||||||
|
onSwap?: (pool: PoolInfo) => void;
|
||||||
|
onCreatePool?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PoolBrowser: React.FC<PoolBrowserProps> = ({
|
||||||
|
onAddLiquidity,
|
||||||
|
onRemoveLiquidity,
|
||||||
|
onSwap,
|
||||||
|
onCreatePool,
|
||||||
|
}) => {
|
||||||
|
const { api, isApiReady } = usePolkadot();
|
||||||
|
const { account } = useWallet();
|
||||||
|
const [pools, setPools] = useState<PoolInfo[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [sortBy, setSortBy] = useState<'tvl' | 'volume' | 'apr'>('tvl');
|
||||||
|
|
||||||
|
const isFounder = account ? isFounderWallet(account.address) : false;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPools = async () => {
|
||||||
|
if (!api || !isApiReady) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const poolsData = await fetchPools(api);
|
||||||
|
setPools(poolsData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load pools:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPools();
|
||||||
|
|
||||||
|
// Refresh pools every 10 seconds
|
||||||
|
const interval = setInterval(loadPools, 10000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [api, isApiReady]);
|
||||||
|
|
||||||
|
const filteredPools = pools.filter((pool) => {
|
||||||
|
if (!searchTerm) return true;
|
||||||
|
const search = searchTerm.toLowerCase();
|
||||||
|
return (
|
||||||
|
pool.asset1Symbol.toLowerCase().includes(search) ||
|
||||||
|
pool.asset2Symbol.toLowerCase().includes(search) ||
|
||||||
|
pool.id.toLowerCase().includes(search)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading && pools.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="text-gray-400">Loading pools...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header with search and create */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||||
|
<div className="relative flex-1 w-full">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search pools by token..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 bg-gray-900 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFounder && onCreatePool && (
|
||||||
|
<button
|
||||||
|
onClick={onCreatePool}
|
||||||
|
className="flex items-center gap-2 px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Create Pool
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pools grid */}
|
||||||
|
{filteredPools.length === 0 ? (
|
||||||
|
<Card className="bg-gray-900/50 border-gray-800">
|
||||||
|
<CardContent className="py-12">
|
||||||
|
<div className="text-center text-gray-400">
|
||||||
|
{searchTerm
|
||||||
|
? 'No pools found matching your search'
|
||||||
|
: 'No liquidity pools available yet'}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
{filteredPools.map((pool) => (
|
||||||
|
<PoolCard
|
||||||
|
key={pool.id}
|
||||||
|
pool={pool}
|
||||||
|
onAddLiquidity={onAddLiquidity}
|
||||||
|
onRemoveLiquidity={onRemoveLiquidity}
|
||||||
|
onSwap={onSwap}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PoolCardProps {
|
||||||
|
pool: PoolInfo;
|
||||||
|
onAddLiquidity?: (pool: PoolInfo) => void;
|
||||||
|
onRemoveLiquidity?: (pool: PoolInfo) => void;
|
||||||
|
onSwap?: (pool: PoolInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PoolCard: React.FC<PoolCardProps> = ({
|
||||||
|
pool,
|
||||||
|
onAddLiquidity,
|
||||||
|
onRemoveLiquidity,
|
||||||
|
onSwap,
|
||||||
|
}) => {
|
||||||
|
const reserve1Display = formatTokenBalance(
|
||||||
|
pool.reserve1,
|
||||||
|
pool.asset1Decimals,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
const reserve2Display = formatTokenBalance(
|
||||||
|
pool.reserve2,
|
||||||
|
pool.asset2Decimals,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate exchange rate
|
||||||
|
const rate =
|
||||||
|
BigInt(pool.reserve1) > BigInt(0)
|
||||||
|
? (Number(pool.reserve2) / Number(pool.reserve1)).toFixed(4)
|
||||||
|
: '0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/50 border-gray-800 hover:border-gray-700 transition-colors">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg font-bold text-white flex items-center gap-2">
|
||||||
|
<span className="text-green-400">{pool.asset1Symbol}</span>
|
||||||
|
<span className="text-gray-500">/</span>
|
||||||
|
<span className="text-yellow-400">{pool.asset2Symbol}</span>
|
||||||
|
</CardTitle>
|
||||||
|
<Badge className="bg-green-500/10 text-green-400 border-green-500/20">
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Reserves */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Reserve {pool.asset1Symbol}</span>
|
||||||
|
<span className="text-white font-mono">
|
||||||
|
{reserve1Display} {pool.asset1Symbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Reserve {pool.asset2Symbol}</span>
|
||||||
|
<span className="text-white font-mono">
|
||||||
|
{reserve2Display} {pool.asset2Symbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Exchange rate */}
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Exchange Rate</span>
|
||||||
|
<span className="text-cyan-400 font-mono">
|
||||||
|
1 {pool.asset1Symbol} = {rate} {pool.asset2Symbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats row */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 pt-2 border-t border-gray-800">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs text-gray-500">Fee</div>
|
||||||
|
<div className="text-sm font-semibold text-white">
|
||||||
|
{pool.feeRate || '0.3'}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs text-gray-500">Volume 24h</div>
|
||||||
|
<div className="text-sm font-semibold text-white">
|
||||||
|
{pool.volume24h || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs text-gray-500">APR</div>
|
||||||
|
<div className="text-sm font-semibold text-green-400">
|
||||||
|
{pool.apr7d || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 pt-2">
|
||||||
|
{onAddLiquidity && (
|
||||||
|
<button
|
||||||
|
onClick={() => onAddLiquidity(pool)}
|
||||||
|
className="px-3 py-2 bg-green-600/20 hover:bg-green-600/30 text-green-400 border border-green-600/30 rounded-lg text-xs font-medium transition-colors flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<Droplet className="w-3 h-3" />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onRemoveLiquidity && (
|
||||||
|
<button
|
||||||
|
onClick={() => onRemoveLiquidity(pool)}
|
||||||
|
className="px-3 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 border border-red-600/30 rounded-lg text-xs font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onSwap && (
|
||||||
|
<button
|
||||||
|
onClick={() => onSwap(pool)}
|
||||||
|
className="px-3 py-2 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border border-blue-600/30 rounded-lg text-xs font-medium transition-colors flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<TrendingUp className="w-3 h-3" />
|
||||||
|
Swap
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
|
import { X, Minus, AlertCircle, Loader2, CheckCircle, Info } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { PoolInfo } from '@/types/dex';
|
||||||
|
import { formatTokenBalance } from '@/utils/dex';
|
||||||
|
|
||||||
|
interface RemoveLiquidityModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
pool: PoolInfo | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error';
|
||||||
|
|
||||||
|
export const RemoveLiquidityModal: React.FC<RemoveLiquidityModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
pool,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const { api, isApiReady } = usePolkadot();
|
||||||
|
const { account, signer } = useWallet();
|
||||||
|
|
||||||
|
const [lpTokenBalance, setLpTokenBalance] = useState<string>('0');
|
||||||
|
const [removePercentage, setRemovePercentage] = useState(25);
|
||||||
|
const [slippage, setSlippage] = useState(1); // 1% default
|
||||||
|
|
||||||
|
const [txStatus, setTxStatus] = useState<TransactionStatus>('idle');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
|
// Reset form when modal closes or pool changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !pool) {
|
||||||
|
setRemovePercentage(25);
|
||||||
|
setTxStatus('idle');
|
||||||
|
setErrorMessage('');
|
||||||
|
}
|
||||||
|
}, [isOpen, pool]);
|
||||||
|
|
||||||
|
// Fetch LP token balance
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLPBalance = async () => {
|
||||||
|
if (!api || !isApiReady || !account || !pool) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get pool account
|
||||||
|
const poolAccount = await api.query.assetConversion.pools([
|
||||||
|
pool.asset1,
|
||||||
|
pool.asset2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (poolAccount.isNone) {
|
||||||
|
setLpTokenBalance('0');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LP token ID is derived from pool ID
|
||||||
|
// For now, we'll query the pool's LP token supply
|
||||||
|
// In a real implementation, you'd need to query the specific LP token for the user
|
||||||
|
const lpAssetId = api.query.assetConversion.nextPoolAssetId
|
||||||
|
? await api.query.assetConversion.nextPoolAssetId()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// This is a simplified version - you'd need to track LP tokens properly
|
||||||
|
setLpTokenBalance('0'); // Placeholder
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch LP balance:', error);
|
||||||
|
setLpTokenBalance('0');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLPBalance();
|
||||||
|
}, [api, isApiReady, account, pool]);
|
||||||
|
|
||||||
|
const calculateOutputAmounts = () => {
|
||||||
|
if (!pool || BigInt(lpTokenBalance) === BigInt(0)) {
|
||||||
|
return { amount1: '0', amount2: '0' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate amounts based on percentage
|
||||||
|
const lpAmount = (BigInt(lpTokenBalance) * BigInt(removePercentage)) / BigInt(100);
|
||||||
|
|
||||||
|
// Simplified calculation - in reality, this depends on total LP supply
|
||||||
|
const totalLiquidity = BigInt(pool.reserve1) + BigInt(pool.reserve2);
|
||||||
|
const userShare = lpAmount;
|
||||||
|
|
||||||
|
// Proportional amounts
|
||||||
|
const amount1 = (BigInt(pool.reserve1) * userShare) / totalLiquidity;
|
||||||
|
const amount2 = (BigInt(pool.reserve2) * userShare) / totalLiquidity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount1: amount1.toString(),
|
||||||
|
amount2: amount2.toString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveLiquidity = async () => {
|
||||||
|
if (!api || !isApiReady || !signer || !account || !pool) {
|
||||||
|
setErrorMessage('Wallet not connected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BigInt(lpTokenBalance) === BigInt(0)) {
|
||||||
|
setErrorMessage('No liquidity to remove');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lpAmount = (BigInt(lpTokenBalance) * BigInt(removePercentage)) / BigInt(100);
|
||||||
|
const { amount1, amount2 } = calculateOutputAmounts();
|
||||||
|
|
||||||
|
// Calculate minimum amounts with slippage tolerance
|
||||||
|
const minAmount1 = (BigInt(amount1) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
||||||
|
const minAmount2 = (BigInt(amount2) * BigInt(100 - slippage * 100)) / BigInt(10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setTxStatus('signing');
|
||||||
|
setErrorMessage('');
|
||||||
|
|
||||||
|
const tx = api.tx.assetConversion.removeLiquidity(
|
||||||
|
pool.asset1,
|
||||||
|
pool.asset2,
|
||||||
|
lpAmount.toString(),
|
||||||
|
minAmount1.toString(),
|
||||||
|
minAmount2.toString(),
|
||||||
|
account
|
||||||
|
);
|
||||||
|
|
||||||
|
setTxStatus('submitting');
|
||||||
|
|
||||||
|
await tx.signAndSend(
|
||||||
|
account,
|
||||||
|
{ signer },
|
||||||
|
({ status, dispatchError }) => {
|
||||||
|
if (status.isInBlock) {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||||
|
setErrorMessage(`${decoded.section}.${decoded.name}: ${decoded.docs}`);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(dispatchError.toString());
|
||||||
|
}
|
||||||
|
setTxStatus('error');
|
||||||
|
} else {
|
||||||
|
setTxStatus('success');
|
||||||
|
setTimeout(() => {
|
||||||
|
onSuccess?.();
|
||||||
|
onClose();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Remove liquidity failed:', error);
|
||||||
|
setErrorMessage(error.message || 'Transaction failed');
|
||||||
|
setTxStatus('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen || !pool) return null;
|
||||||
|
|
||||||
|
const { amount1, amount2 } = calculateOutputAmounts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
|
<Card className="bg-gray-900 border-gray-800 max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<CardHeader className="border-b border-gray-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl font-bold text-white">
|
||||||
|
Remove Liquidity
|
||||||
|
</CardTitle>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-white transition-colors"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400 mt-2">
|
||||||
|
{pool.asset1Symbol} / {pool.asset2Symbol} Pool
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6 pt-6">
|
||||||
|
{/* Info Banner */}
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||||
|
<Info className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-blue-400">
|
||||||
|
Remove liquidity to receive your tokens back. You'll burn LP tokens in proportion to your withdrawal.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LP Token Balance */}
|
||||||
|
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700">
|
||||||
|
<div className="text-sm text-gray-400 mb-1">Your LP Tokens</div>
|
||||||
|
<div className="text-2xl font-bold text-white font-mono">
|
||||||
|
{formatTokenBalance(lpTokenBalance, 12, 6)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Percentage Selector */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm text-gray-400">Remove Amount</label>
|
||||||
|
<span className="text-lg font-bold text-white">{removePercentage}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={removePercentage}
|
||||||
|
onChange={(e) => setRemovePercentage(Number(e.target.value))}
|
||||||
|
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{[25, 50, 75, 100].map((value) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
onClick={() => setRemovePercentage(value)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
removePercentage === value
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
{value}%
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-800 border border-gray-700 flex items-center justify-center">
|
||||||
|
<Minus className="w-5 h-5 text-red-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Output Preview */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-sm text-gray-400 mb-2">You will receive</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-gray-400">{pool.asset1Symbol}</span>
|
||||||
|
<span className="text-white font-mono text-lg">
|
||||||
|
{formatTokenBalance(amount1, pool.asset1Decimals, 6)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-gray-400">{pool.asset2Symbol}</span>
|
||||||
|
<span className="text-white font-mono text-lg">
|
||||||
|
{formatTokenBalance(amount2, pool.asset2Decimals, 6)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slippage Tolerance */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Slippage Tolerance</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[0.5, 1, 2].map((value) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
onClick={() => setSlippage(value)}
|
||||||
|
className={`flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
slippage === value
|
||||||
|
? 'bg-green-600 text-white'
|
||||||
|
: 'bg-gray-800 text-gray-400 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
{value}%
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-red-400">{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<span className="text-sm text-green-400">
|
||||||
|
Liquidity removed successfully!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 px-6 py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-lg transition-colors border border-gray-700"
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveLiquidity}
|
||||||
|
className="flex-1 px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors font-medium flex items-center justify-center gap-2"
|
||||||
|
disabled={
|
||||||
|
txStatus === 'signing' ||
|
||||||
|
txStatus === 'submitting' ||
|
||||||
|
txStatus === 'success' ||
|
||||||
|
BigInt(lpTokenBalance) === BigInt(0)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{txStatus === 'signing' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Signing...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'submitting' && (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Removing...
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{txStatus === 'idle' && 'Remove Liquidity'}
|
||||||
|
{txStatus === 'error' && 'Retry'}
|
||||||
|
{txStatus === 'success' && (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
Success
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,641 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
|
import { ArrowDownUp, AlertCircle, Loader2, CheckCircle, Info, Settings, AlertTriangle } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { PoolInfo } from '@/types/dex';
|
||||||
|
import {
|
||||||
|
parseTokenInput,
|
||||||
|
formatTokenBalance,
|
||||||
|
getAmountOut,
|
||||||
|
calculatePriceImpact,
|
||||||
|
} from '@/utils/dex';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
|
interface SwapInterfaceProps {
|
||||||
|
initialPool?: PoolInfo | null;
|
||||||
|
pools: PoolInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error';
|
||||||
|
|
||||||
|
// User-facing tokens (wHEZ is hidden from users, shown as HEZ)
|
||||||
|
const USER_TOKENS = [
|
||||||
|
{ symbol: 'HEZ', emoji: '🟡', assetId: 0, name: 'HEZ', decimals: 12, displaySymbol: 'HEZ' }, // actually wHEZ (asset 0)
|
||||||
|
{ symbol: 'PEZ', emoji: '🟣', assetId: 1, name: 'PEZ', decimals: 12, displaySymbol: 'PEZ' },
|
||||||
|
{ symbol: 'USDT', emoji: '💵', assetId: 2, name: 'USDT', decimals: 6, displaySymbol: 'USDT' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SwapInterface: React.FC<SwapInterfaceProps> = ({ initialPool, pools }) => {
|
||||||
|
const { api, isApiReady } = usePolkadot();
|
||||||
|
const { account, signer } = useWallet();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [fromToken, setFromToken] = useState('HEZ');
|
||||||
|
const [toToken, setToToken] = useState('PEZ');
|
||||||
|
const [fromAmount, setFromAmount] = useState('');
|
||||||
|
const [toAmount, setToAmount] = useState('');
|
||||||
|
const [slippage, setSlippage] = useState(0.5); // 0.5% default
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
|
|
||||||
|
const [fromBalance, setFromBalance] = useState<string>('0');
|
||||||
|
const [toBalance, setToBalance] = useState<string>('0');
|
||||||
|
|
||||||
|
const [txStatus, setTxStatus] = useState<TransactionStatus>('idle');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
|
// Get asset IDs (for pool lookup)
|
||||||
|
const getAssetId = (symbol: string) => {
|
||||||
|
const token = USER_TOKENS.find(t => t.symbol === symbol);
|
||||||
|
return token?.assetId ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromAssetId = getAssetId(fromToken);
|
||||||
|
const toAssetId = getAssetId(toToken);
|
||||||
|
|
||||||
|
// Find active pool for selected pair
|
||||||
|
const activePool = pools.find(
|
||||||
|
(p) =>
|
||||||
|
(p.asset1 === fromAssetId && p.asset2 === toAssetId) ||
|
||||||
|
(p.asset1 === toAssetId && p.asset2 === fromAssetId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get token info
|
||||||
|
const fromTokenInfo = USER_TOKENS.find(t => t.symbol === fromToken);
|
||||||
|
const toTokenInfo = USER_TOKENS.find(t => t.symbol === toToken);
|
||||||
|
|
||||||
|
// Fetch balances
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchBalances = async () => {
|
||||||
|
if (!api || !isApiReady || !account) return;
|
||||||
|
|
||||||
|
// For HEZ, fetch native balance (not wHEZ asset balance)
|
||||||
|
if (fromToken === 'HEZ') {
|
||||||
|
try {
|
||||||
|
const balance = await api.query.system.account(account);
|
||||||
|
const freeBalance = balance.data.free.toString();
|
||||||
|
setFromBalance(freeBalance);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch HEZ balance:', error);
|
||||||
|
setFromBalance('0');
|
||||||
|
}
|
||||||
|
} else if (fromAssetId !== null) {
|
||||||
|
try {
|
||||||
|
const balanceData = await api.query.assets.account(fromAssetId, account);
|
||||||
|
setFromBalance(balanceData.isSome ? balanceData.unwrap().balance.toString() : '0');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch from balance:', error);
|
||||||
|
setFromBalance('0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HEZ, fetch native balance
|
||||||
|
if (toToken === 'HEZ') {
|
||||||
|
try {
|
||||||
|
const balance = await api.query.system.account(account);
|
||||||
|
const freeBalance = balance.data.free.toString();
|
||||||
|
setToBalance(freeBalance);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch HEZ balance:', error);
|
||||||
|
setToBalance('0');
|
||||||
|
}
|
||||||
|
} else if (toAssetId !== null) {
|
||||||
|
try {
|
||||||
|
const balanceData = await api.query.assets.account(toAssetId, account);
|
||||||
|
setToBalance(balanceData.isSome ? balanceData.unwrap().balance.toString() : '0');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch to balance:', error);
|
||||||
|
setToBalance('0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchBalances();
|
||||||
|
}, [api, isApiReady, account, fromToken, toToken, fromAssetId, toAssetId]);
|
||||||
|
|
||||||
|
// Calculate output amount when input changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fromAmount || !activePool || !fromTokenInfo || !toTokenInfo) {
|
||||||
|
setToAmount('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fromAmountRaw = parseTokenInput(fromAmount, fromTokenInfo.decimals);
|
||||||
|
|
||||||
|
// Determine direction and calculate output
|
||||||
|
const isForward = activePool.asset1 === fromAssetId;
|
||||||
|
const reserveIn = isForward ? activePool.reserve1 : activePool.reserve2;
|
||||||
|
const reserveOut = isForward ? activePool.reserve2 : activePool.reserve1;
|
||||||
|
|
||||||
|
const toAmountRaw = getAmountOut(fromAmountRaw, reserveIn, reserveOut, 30); // 3% fee
|
||||||
|
const toAmountDisplay = formatTokenBalance(toAmountRaw, toTokenInfo.decimals, 6);
|
||||||
|
|
||||||
|
setToAmount(toAmountDisplay);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to calculate output:', error);
|
||||||
|
setToAmount('');
|
||||||
|
}
|
||||||
|
}, [fromAmount, activePool, fromTokenInfo, toTokenInfo, fromAssetId, toAssetId]);
|
||||||
|
|
||||||
|
// Calculate price impact
|
||||||
|
const priceImpact = React.useMemo(() => {
|
||||||
|
if (!fromAmount || !activePool || !fromAssetId || !toAssetId || !fromTokenInfo) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fromAmountRaw = parseTokenInput(fromAmount, fromTokenInfo.decimals);
|
||||||
|
const isForward = activePool.asset1 === fromAssetId;
|
||||||
|
const reserveIn = isForward ? activePool.reserve1 : activePool.reserve2;
|
||||||
|
const reserveOut = isForward ? activePool.reserve2 : activePool.reserve1;
|
||||||
|
|
||||||
|
return parseFloat(calculatePriceImpact(reserveIn, reserveOut, fromAmountRaw));
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}, [fromAmount, activePool, fromAssetId, toAssetId, fromTokenInfo]);
|
||||||
|
|
||||||
|
// Check if user has insufficient balance
|
||||||
|
const hasInsufficientBalance = React.useMemo(() => {
|
||||||
|
const fromAmountNum = parseFloat(fromAmount || '0');
|
||||||
|
const fromBalanceNum = parseFloat(formatTokenBalance(fromBalance, fromTokenInfo?.decimals ?? 12, 6));
|
||||||
|
return fromAmountNum > 0 && fromAmountNum > fromBalanceNum;
|
||||||
|
}, [fromAmount, fromBalance, fromTokenInfo]);
|
||||||
|
|
||||||
|
const handleSwapDirection = () => {
|
||||||
|
const tempToken = fromToken;
|
||||||
|
const tempAmount = fromAmount;
|
||||||
|
const tempBalance = fromBalance;
|
||||||
|
|
||||||
|
setFromToken(toToken);
|
||||||
|
setToToken(tempToken);
|
||||||
|
setFromAmount(toAmount);
|
||||||
|
setFromBalance(toBalance);
|
||||||
|
setToBalance(tempBalance);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaxClick = () => {
|
||||||
|
if (fromTokenInfo) {
|
||||||
|
const maxAmount = formatTokenBalance(fromBalance, fromTokenInfo.decimals, 6);
|
||||||
|
setFromAmount(maxAmount);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmSwap = async () => {
|
||||||
|
if (!api || !signer || !account || !fromTokenInfo || !toTokenInfo) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Please connect your wallet',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activePool) {
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'No liquidity pool available for this pair',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTxStatus('signing');
|
||||||
|
setShowConfirm(false);
|
||||||
|
setErrorMessage('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const amountIn = parseTokenInput(fromAmount, fromTokenInfo.decimals);
|
||||||
|
const minAmountOut = parseTokenInput(
|
||||||
|
(parseFloat(toAmount) * (1 - slippage / 100)).toString(),
|
||||||
|
toTokenInfo.decimals
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('💰 Swap transaction:', {
|
||||||
|
from: fromToken,
|
||||||
|
to: toToken,
|
||||||
|
amount: fromAmount,
|
||||||
|
minOut: minAmountOut.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let tx;
|
||||||
|
|
||||||
|
if (fromToken === 'HEZ' && toToken === 'PEZ') {
|
||||||
|
// HEZ → PEZ: wrap(HEZ→wHEZ) then swap(wHEZ→PEZ)
|
||||||
|
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
|
||||||
|
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
|
[0, 1], // wHEZ → PEZ
|
||||||
|
amountIn.toString(),
|
||||||
|
minAmountOut.toString(),
|
||||||
|
account,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
tx = api.tx.utility.batchAll([wrapTx, swapTx]);
|
||||||
|
|
||||||
|
} else if (fromToken === 'PEZ' && toToken === 'HEZ') {
|
||||||
|
// PEZ → HEZ: swap(PEZ→wHEZ) then unwrap(wHEZ→HEZ)
|
||||||
|
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
|
[1, 0], // PEZ → wHEZ
|
||||||
|
amountIn.toString(),
|
||||||
|
minAmountOut.toString(),
|
||||||
|
account,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const unwrapTx = api.tx.tokenWrapper.unwrap(minAmountOut.toString());
|
||||||
|
tx = api.tx.utility.batchAll([swapTx, unwrapTx]);
|
||||||
|
|
||||||
|
} else if (fromToken === 'HEZ') {
|
||||||
|
// HEZ → Any Asset: wrap(HEZ→wHEZ) then swap(wHEZ→Asset)
|
||||||
|
const wrapTx = api.tx.tokenWrapper.wrap(amountIn.toString());
|
||||||
|
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
|
[0, toAssetId!], // wHEZ → target asset
|
||||||
|
amountIn.toString(),
|
||||||
|
minAmountOut.toString(),
|
||||||
|
account,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
tx = api.tx.utility.batchAll([wrapTx, swapTx]);
|
||||||
|
|
||||||
|
} else if (toToken === 'HEZ') {
|
||||||
|
// Any Asset → HEZ: swap(Asset→wHEZ) then unwrap(wHEZ→HEZ)
|
||||||
|
const swapTx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
|
[fromAssetId!, 0], // source asset → wHEZ
|
||||||
|
amountIn.toString(),
|
||||||
|
minAmountOut.toString(),
|
||||||
|
account,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const unwrapTx = api.tx.tokenWrapper.unwrap(minAmountOut.toString());
|
||||||
|
tx = api.tx.utility.batchAll([swapTx, unwrapTx]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Direct swap between assets (PEZ ↔ USDT, etc.)
|
||||||
|
tx = api.tx.assetConversion.swapExactTokensForTokens(
|
||||||
|
[fromAssetId!, toAssetId!],
|
||||||
|
amountIn.toString(),
|
||||||
|
minAmountOut.toString(),
|
||||||
|
account,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTxStatus('submitting');
|
||||||
|
|
||||||
|
await tx.signAndSend(
|
||||||
|
account,
|
||||||
|
{ signer },
|
||||||
|
({ status, dispatchError }) => {
|
||||||
|
if (status.isInBlock) {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||||
|
setErrorMessage(`${decoded.section}.${decoded.name}: ${decoded.docs}`);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(dispatchError.toString());
|
||||||
|
}
|
||||||
|
setTxStatus('error');
|
||||||
|
toast({
|
||||||
|
title: 'Transaction Failed',
|
||||||
|
description: errorMessage,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTxStatus('success');
|
||||||
|
toast({
|
||||||
|
title: 'Success!',
|
||||||
|
description: `Swapped ${fromAmount} ${fromToken} for ~${toAmount} ${toToken}`,
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
setFromAmount('');
|
||||||
|
setToAmount('');
|
||||||
|
setTxStatus('idle');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Swap failed:', error);
|
||||||
|
setErrorMessage(error.message || 'Transaction failed');
|
||||||
|
setTxStatus('error');
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: error.message || 'Swap transaction failed',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeRate = activePool && fromTokenInfo && toTokenInfo
|
||||||
|
? (
|
||||||
|
parseFloat(formatTokenBalance(activePool.reserve2, toTokenInfo.decimals, 6)) /
|
||||||
|
parseFloat(formatTokenBalance(activePool.reserve1, fromTokenInfo.decimals, 6))
|
||||||
|
).toFixed(6)
|
||||||
|
: '0';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-lg mx-auto">
|
||||||
|
{/* Transaction Loading Overlay */}
|
||||||
|
{(txStatus === 'signing' || txStatus === 'submitting') && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<Loader2 className="w-16 h-16 animate-spin text-green-400" />
|
||||||
|
<p className="text-white text-xl font-semibold">
|
||||||
|
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing swap...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="bg-gray-900 border-gray-800">
|
||||||
|
<CardHeader className="border-b border-gray-800">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl font-bold text-white">Swap Tokens</CardTitle>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setShowSettings(true)}>
|
||||||
|
<Settings className="h-5 w-5 text-gray-400" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-4 pt-6">
|
||||||
|
{!account && (
|
||||||
|
<Alert className="bg-yellow-500/10 border-yellow-500/30">
|
||||||
|
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||||
|
<AlertDescription className="text-yellow-300">
|
||||||
|
Please connect your wallet to swap tokens
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* From Token */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">From</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Balance: {formatTokenBalance(fromBalance, fromTokenInfo?.decimals ?? 12, 4)} {fromToken}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg space-y-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={fromAmount}
|
||||||
|
onChange={(e) => setFromAmount(e.target.value)}
|
||||||
|
placeholder="0.0"
|
||||||
|
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600 focus-visible:ring-0"
|
||||||
|
disabled={!account}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={fromToken}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setFromToken(value);
|
||||||
|
if (value === toToken) {
|
||||||
|
const otherToken = USER_TOKENS.find(t => t.symbol !== value);
|
||||||
|
if (otherToken) setToToken(otherToken.symbol);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!account}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="min-w-[140px] border-gray-600 bg-gray-900">
|
||||||
|
<SelectValue>
|
||||||
|
{(() => {
|
||||||
|
const token = USER_TOKENS.find(t => t.symbol === fromToken);
|
||||||
|
return <span className="flex items-center gap-2">{token?.emoji} {token?.displaySymbol}</span>;
|
||||||
|
})()}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-gray-900 border-gray-700">
|
||||||
|
{USER_TOKENS.map((token) => (
|
||||||
|
<SelectItem key={token.symbol} value={token.symbol}>
|
||||||
|
<span className="flex items-center gap-2">{token.emoji} {token.name}</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleMaxClick}
|
||||||
|
className="px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded border border-green-600/30 transition-colors"
|
||||||
|
disabled={!account}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swap Direction Button */}
|
||||||
|
<div className="flex justify-center -my-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleSwapDirection}
|
||||||
|
className="rounded-full bg-gray-800 border-2 border-gray-700 hover:bg-gray-700"
|
||||||
|
disabled={!account}
|
||||||
|
>
|
||||||
|
<ArrowDownUp className="h-5 w-5 text-gray-300" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* To Token */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">To</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Balance: {formatTokenBalance(toBalance, toTokenInfo?.decimals ?? 12, 4)} {toToken}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={toAmount}
|
||||||
|
readOnly
|
||||||
|
placeholder="0.0"
|
||||||
|
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={toToken}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setToToken(value);
|
||||||
|
if (value === fromToken) {
|
||||||
|
const otherToken = USER_TOKENS.find(t => t.symbol !== value);
|
||||||
|
if (otherToken) setFromToken(otherToken.symbol);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!account}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="min-w-[140px] border-gray-600 bg-gray-900">
|
||||||
|
<SelectValue>
|
||||||
|
{(() => {
|
||||||
|
const token = USER_TOKENS.find(t => t.symbol === toToken);
|
||||||
|
return <span className="flex items-center gap-2">{token?.emoji} {token?.displaySymbol}</span>;
|
||||||
|
})()}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-gray-900 border-gray-700">
|
||||||
|
{USER_TOKENS.map((token) => (
|
||||||
|
<SelectItem key={token.symbol} value={token.symbol}>
|
||||||
|
<span className="flex items-center gap-2">{token.emoji} {token.name}</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swap Details */}
|
||||||
|
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400 flex items-center gap-1">
|
||||||
|
<Info className="w-3 h-3" />
|
||||||
|
Exchange Rate
|
||||||
|
</span>
|
||||||
|
<span className="text-white">
|
||||||
|
{activePool ? `1 ${fromToken} = ${exchangeRate} ${toToken}` : 'No pool available'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fromAmount && parseFloat(fromAmount) > 0 && priceImpact > 0 && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-400 flex items-center gap-1">
|
||||||
|
<AlertTriangle className={`w-3 h-3 ${
|
||||||
|
priceImpact < 1 ? 'text-green-500' :
|
||||||
|
priceImpact < 5 ? 'text-yellow-500' :
|
||||||
|
'text-red-500'
|
||||||
|
}`} />
|
||||||
|
Price Impact
|
||||||
|
</span>
|
||||||
|
<span className={`font-semibold ${
|
||||||
|
priceImpact < 1 ? 'text-green-400' :
|
||||||
|
priceImpact < 5 ? 'text-yellow-400' :
|
||||||
|
'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{priceImpact < 0.01 ? '<0.01%' : `${priceImpact.toFixed(2)}%`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between pt-2 border-t border-gray-700">
|
||||||
|
<span className="text-gray-400">Slippage Tolerance</span>
|
||||||
|
<span className="text-blue-400">{slippage}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warnings */}
|
||||||
|
{hasInsufficientBalance && (
|
||||||
|
<Alert className="bg-red-900/20 border-red-500/30">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||||
|
<AlertDescription className="text-red-300 text-sm">
|
||||||
|
Insufficient {fromToken} balance
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{priceImpact >= 5 && !hasInsufficientBalance && (
|
||||||
|
<Alert className="bg-red-900/20 border-red-500/30">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||||
|
<AlertDescription className="text-red-300 text-sm">
|
||||||
|
High price impact! Consider a smaller amount.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Swap Button */}
|
||||||
|
<Button
|
||||||
|
className="w-full h-12 text-lg"
|
||||||
|
onClick={() => setShowConfirm(true)}
|
||||||
|
disabled={
|
||||||
|
!account ||
|
||||||
|
!fromAmount ||
|
||||||
|
parseFloat(fromAmount) <= 0 ||
|
||||||
|
!activePool ||
|
||||||
|
hasInsufficientBalance ||
|
||||||
|
txStatus === 'signing' ||
|
||||||
|
txStatus === 'submitting'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!account
|
||||||
|
? 'Connect Wallet'
|
||||||
|
: hasInsufficientBalance
|
||||||
|
? `Insufficient ${fromToken} Balance`
|
||||||
|
: !activePool
|
||||||
|
? 'No Pool Available'
|
||||||
|
: 'Swap Tokens'}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Settings Dialog */}
|
||||||
|
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||||
|
<DialogContent className="bg-gray-900 border-gray-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Swap Settings</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-300">Slippage Tolerance</label>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
{[0.1, 0.5, 1.0, 2.0].map(val => (
|
||||||
|
<Button
|
||||||
|
key={val}
|
||||||
|
variant={slippage === val ? 'default' : 'outline'}
|
||||||
|
onClick={() => setSlippage(val)}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{val}%
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Confirm Dialog */}
|
||||||
|
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
|
||||||
|
<DialogContent className="bg-gray-900 border-gray-800">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Confirm Swap</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-gray-800 border border-gray-700 rounded-lg space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">You Pay</span>
|
||||||
|
<span className="font-bold text-white">{fromAmount} {fromToken}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-300">You Receive</span>
|
||||||
|
<span className="font-bold text-white">{toAmount} {toToken}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm pt-2 border-t border-gray-700">
|
||||||
|
<span className="text-gray-400">Exchange Rate</span>
|
||||||
|
<span className="text-gray-400">1 {fromToken} = {exchangeRate} {toToken}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Slippage</span>
|
||||||
|
<span className="text-gray-400">{slippage}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleConfirmSwap}
|
||||||
|
disabled={txStatus === 'signing' || txStatus === 'submitting'}
|
||||||
|
>
|
||||||
|
{txStatus === 'signing' ? 'Signing...' : txStatus === 'submitting' ? 'Swapping...' : 'Confirm Swap'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -78,7 +78,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
.from('admin_roles')
|
.from('admin_roles')
|
||||||
.select('role')
|
.select('role')
|
||||||
.eq('user_id', user.id)
|
.eq('user_id', user.id)
|
||||||
.single();
|
.maybeSingle();
|
||||||
|
|
||||||
const adminStatus = !error && data && ['admin', 'super_admin'].includes(data.role);
|
const adminStatus = !error && data && ['admin', 'super_admin'].includes(data.role);
|
||||||
setIsAdmin(adminStatus);
|
setIsAdmin(adminStatus);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr
|
|||||||
import { usePolkadot } from './PolkadotContext';
|
import { usePolkadot } from './PolkadotContext';
|
||||||
import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@/lib/wallet';
|
import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@/lib/wallet';
|
||||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||||
|
import type { Signer } from '@polkadot/api/types';
|
||||||
|
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||||
|
|
||||||
interface TokenBalances {
|
interface TokenBalances {
|
||||||
HEZ: string;
|
HEZ: string;
|
||||||
@@ -23,6 +25,7 @@ interface WalletContextType {
|
|||||||
balance: string; // Legacy: HEZ balance
|
balance: string; // Legacy: HEZ balance
|
||||||
balances: TokenBalances; // All token balances
|
balances: TokenBalances; // All token balances
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
signer: Signer | null; // Polkadot.js signer for transactions
|
||||||
connectWallet: () => Promise<void>;
|
connectWallet: () => Promise<void>;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
switchAccount: (account: InjectedAccountWithMeta) => void;
|
switchAccount: (account: InjectedAccountWithMeta) => void;
|
||||||
@@ -46,6 +49,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
const [balance, setBalance] = useState<string>('0');
|
const [balance, setBalance] = useState<string>('0');
|
||||||
const [balances, setBalances] = useState<TokenBalances>({ HEZ: '0', PEZ: '0', wHEZ: '0', USDT: '0' });
|
const [balances, setBalances] = useState<TokenBalances>({ HEZ: '0', PEZ: '0', wHEZ: '0', USDT: '0' });
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [signer, setSigner] = useState<Signer | null>(null);
|
||||||
|
|
||||||
// Fetch all token balances when account changes
|
// Fetch all token balances when account changes
|
||||||
const updateBalance = useCallback(async (address: string) => {
|
const updateBalance = useCallback(async (address: string) => {
|
||||||
@@ -203,6 +207,26 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
}
|
}
|
||||||
}, [polkadot.selectedAccount]);
|
}, [polkadot.selectedAccount]);
|
||||||
|
|
||||||
|
// Get signer from extension when account changes
|
||||||
|
useEffect(() => {
|
||||||
|
const getSigner = async () => {
|
||||||
|
if (polkadot.selectedAccount) {
|
||||||
|
try {
|
||||||
|
const injector = await web3FromAddress(polkadot.selectedAccount.address);
|
||||||
|
setSigner(injector.signer);
|
||||||
|
console.log('✅ Signer obtained for', polkadot.selectedAccount.address);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get signer:', error);
|
||||||
|
setSigner(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSigner(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getSigner();
|
||||||
|
}, [polkadot.selectedAccount]);
|
||||||
|
|
||||||
// Update balance when selected account changes
|
// Update balance when selected account changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🔄 WalletContext useEffect triggered!', {
|
console.log('🔄 WalletContext useEffect triggered!', {
|
||||||
@@ -237,6 +261,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
balance,
|
balance,
|
||||||
balances,
|
balances,
|
||||||
error: error || polkadot.error,
|
error: error || polkadot.error,
|
||||||
|
signer,
|
||||||
connectWallet,
|
connectWallet,
|
||||||
disconnect,
|
disconnect,
|
||||||
switchAccount,
|
switchAccount,
|
||||||
|
|||||||
+10
-15
@@ -2,30 +2,25 @@ import i18n from 'i18next';
|
|||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
// Import all language translations
|
// Import shared translations
|
||||||
import enTranslations from './locales/en';
|
import { translations } from '@pezkuwi/i18n';
|
||||||
import trTranslations from './locales/tr';
|
|
||||||
import kmrTranslations from './locales/kmr';
|
|
||||||
import ckbTranslations from './locales/ckb';
|
|
||||||
import arTranslations from './locales/ar';
|
|
||||||
import faTranslations from './locales/fa';
|
|
||||||
|
|
||||||
export const languages = {
|
export const languages = {
|
||||||
en: { name: 'English', flag: '🇬🇧', dir: 'ltr' },
|
en: { name: 'English', flag: '🇬🇧', dir: 'ltr' },
|
||||||
tr: { name: 'Türkçe', flag: '🇹🇷', dir: 'ltr' },
|
tr: { name: 'Türkçe', flag: '🇹🇷', dir: 'ltr' },
|
||||||
kmr: { name: 'Kurdî (Kurmancî)', flag: '☀️', dir: 'ltr' },
|
'ku-kurmanji': { name: 'Kurdî (Kurmancî)', flag: '☀️', dir: 'ltr' },
|
||||||
ckb: { name: 'کوردی (سۆرانی)', flag: '☀️', dir: 'rtl' },
|
'ku-sorani': { name: 'کوردی (سۆرانی)', flag: '☀️', dir: 'rtl' },
|
||||||
ar: { name: 'العربية', flag: '🇸🇦', dir: 'rtl' },
|
ar: { name: 'العربية', flag: '🇸🇦', dir: 'rtl' },
|
||||||
fa: { name: 'فارسی', flag: '🇮🇷', dir: 'rtl' }
|
fa: { name: 'فارسی', flag: '🇮🇷', dir: 'rtl' }
|
||||||
};
|
};
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
en: { translation: enTranslations },
|
en: { translation: translations.en },
|
||||||
tr: { translation: trTranslations },
|
tr: { translation: translations.tr },
|
||||||
kmr: { translation: kmrTranslations },
|
'ku-kurmanji': { translation: translations['ku-kurmanji'] },
|
||||||
ckb: { translation: ckbTranslations },
|
'ku-sorani': { translation: translations['ku-sorani'] },
|
||||||
ar: { translation: arTranslations },
|
ar: { translation: translations.ar },
|
||||||
fa: { translation: faTranslations }
|
fa: { translation: translations.fa }
|
||||||
};
|
};
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ export const TIKI_SCORES: Record<string, number> = {
|
|||||||
SerokiMeclise: 150,
|
SerokiMeclise: 150,
|
||||||
SerokWeziran: 125,
|
SerokWeziran: 125,
|
||||||
Dozger: 120,
|
Dozger: 120,
|
||||||
Serok: 100,
|
|
||||||
Wezir: 100,
|
Wezir: 100,
|
||||||
WezireDarayiye: 100,
|
WezireDarayiye: 100,
|
||||||
WezireParez: 100,
|
WezireParez: 100,
|
||||||
|
|||||||
@@ -44,9 +44,12 @@ export default function Dashboard() {
|
|||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('id', user.id)
|
.eq('id', user.id)
|
||||||
.single();
|
.maybeSingle();
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) {
|
||||||
|
console.error('Profile fetch error:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-sync user metadata from Auth to profiles if missing
|
// Auto-sync user metadata from Auth to profiles if missing
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function ProfileSettings() {
|
|||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('id', user?.id)
|
.eq('id', user?.id)
|
||||||
.single();
|
.maybeSingle();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error loading profile:', error);
|
console.error('Error loading profile:', error);
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
export interface TokenInfo {
|
||||||
|
id: number;
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
decimals: number;
|
||||||
|
logo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PoolInfo {
|
||||||
|
id: string; // asset1-asset2 pair
|
||||||
|
asset1: number;
|
||||||
|
asset2: number;
|
||||||
|
asset1Symbol: string;
|
||||||
|
asset2Symbol: string;
|
||||||
|
asset1Decimals: number;
|
||||||
|
asset2Decimals: number;
|
||||||
|
reserve1: string; // Raw balance string
|
||||||
|
reserve2: string;
|
||||||
|
lpTokenSupply: string;
|
||||||
|
volume24h?: string;
|
||||||
|
tvl?: string;
|
||||||
|
apr7d?: string;
|
||||||
|
feeRate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserLiquidityPosition {
|
||||||
|
poolId: string;
|
||||||
|
asset1: number;
|
||||||
|
asset2: number;
|
||||||
|
lpTokenBalance: string;
|
||||||
|
shareOfPool: string; // Percentage as string (e.g., "2.5")
|
||||||
|
asset1Amount: string;
|
||||||
|
asset2Amount: string;
|
||||||
|
valueUSD?: string;
|
||||||
|
feesEarned?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwapQuote {
|
||||||
|
amountIn: string;
|
||||||
|
amountOut: string;
|
||||||
|
path: number[]; // Asset IDs in route
|
||||||
|
priceImpact: string; // Percentage as string
|
||||||
|
minimumReceived: string; // After slippage
|
||||||
|
route: string; // Human readable (e.g., "wHEZ → PEZ → wUSDT")
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddLiquidityParams {
|
||||||
|
asset1: number;
|
||||||
|
asset2: number;
|
||||||
|
amount1: string;
|
||||||
|
amount2: string;
|
||||||
|
amount1Min: string;
|
||||||
|
amount2Min: string;
|
||||||
|
recipient: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveLiquidityParams {
|
||||||
|
asset1: number;
|
||||||
|
asset2: number;
|
||||||
|
lpTokenAmount: string;
|
||||||
|
amount1Min: string;
|
||||||
|
amount2Min: string;
|
||||||
|
recipient: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwapParams {
|
||||||
|
path: number[];
|
||||||
|
amountIn: string;
|
||||||
|
amountOutMin: string;
|
||||||
|
recipient: string;
|
||||||
|
deadline?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PoolCreationParams {
|
||||||
|
asset1: number;
|
||||||
|
asset2: number;
|
||||||
|
feeRate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Known tokens on testnet
|
||||||
|
export const KNOWN_TOKENS: Record<number, TokenInfo> = {
|
||||||
|
0: {
|
||||||
|
id: 0,
|
||||||
|
symbol: 'wHEZ',
|
||||||
|
name: 'Wrapped HEZ',
|
||||||
|
decimals: 12,
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
symbol: 'PEZ',
|
||||||
|
name: 'Pezkuwi Token',
|
||||||
|
decimals: 12,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
symbol: 'wUSDT',
|
||||||
|
name: 'Wrapped USDT',
|
||||||
|
decimals: 6,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transaction status
|
||||||
|
export enum TransactionStatus {
|
||||||
|
IDLE = 'idle',
|
||||||
|
PENDING = 'pending',
|
||||||
|
SUCCESS = 'success',
|
||||||
|
ERROR = 'error',
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Authentication and Authorization Utilities
|
||||||
|
* Security-critical: Founder wallet detection and permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// SECURITY: Founder wallet address for beta testnet
|
||||||
|
// This address has sudo rights and can perform privileged operations
|
||||||
|
export const FOUNDER_ADDRESS = '5GgTgG9sRmPQAYU1RsTejZYnZRjwzKZKWD3awtuqjHioki45';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if given address is the founder wallet
|
||||||
|
* @param address - Substrate address to check
|
||||||
|
* @returns true if address matches founder, false otherwise
|
||||||
|
*/
|
||||||
|
export const isFounderWallet = (address: string | null | undefined): boolean => {
|
||||||
|
if (!address) return false;
|
||||||
|
return address === FOUNDER_ADDRESS;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate substrate address format
|
||||||
|
* @param address - Address to validate
|
||||||
|
* @returns true if address is valid format
|
||||||
|
*/
|
||||||
|
export const isValidSubstrateAddress = (address: string): boolean => {
|
||||||
|
// Substrate addresses start with 5 and are 47-48 characters
|
||||||
|
return /^5[a-zA-Z0-9]{46,47}$/.test(address);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission levels for DEX operations
|
||||||
|
*/
|
||||||
|
export enum DexPermission {
|
||||||
|
// Anyone can perform these
|
||||||
|
VIEW_POOLS = 'view_pools',
|
||||||
|
ADD_LIQUIDITY = 'add_liquidity',
|
||||||
|
REMOVE_LIQUIDITY = 'remove_liquidity',
|
||||||
|
SWAP = 'swap',
|
||||||
|
|
||||||
|
// Only founder can perform these
|
||||||
|
CREATE_POOL = 'create_pool',
|
||||||
|
SET_FEE_RATE = 'set_fee_rate',
|
||||||
|
PAUSE_POOL = 'pause_pool',
|
||||||
|
WHITELIST_TOKEN = 'whitelist_token',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has permission for a specific DEX operation
|
||||||
|
* @param address - User's wallet address
|
||||||
|
* @param permission - Required permission level
|
||||||
|
* @returns true if user has permission
|
||||||
|
*/
|
||||||
|
export const hasPermission = (
|
||||||
|
address: string | null | undefined,
|
||||||
|
permission: DexPermission
|
||||||
|
): boolean => {
|
||||||
|
if (!address || !isValidSubstrateAddress(address)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const founderOnly = [
|
||||||
|
DexPermission.CREATE_POOL,
|
||||||
|
DexPermission.SET_FEE_RATE,
|
||||||
|
DexPermission.PAUSE_POOL,
|
||||||
|
DexPermission.WHITELIST_TOKEN,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Founder-only operations
|
||||||
|
if (founderOnly.includes(permission)) {
|
||||||
|
return isFounderWallet(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everyone can view and trade
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user role string for display
|
||||||
|
* @param address - User's wallet address
|
||||||
|
* @returns Human-readable role
|
||||||
|
*/
|
||||||
|
export const getUserRole = (address: string | null | undefined): string => {
|
||||||
|
if (!address) return 'Guest';
|
||||||
|
if (isFounderWallet(address)) return 'Founder (Admin)';
|
||||||
|
return 'User';
|
||||||
|
};
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import { ApiPromise } from '@polkadot/api';
|
||||||
|
import { KNOWN_TOKENS, PoolInfo, SwapQuote } from '../types/dex';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format balance with proper decimals
|
||||||
|
* @param balance - Raw balance string
|
||||||
|
* @param decimals - Token decimals
|
||||||
|
* @param precision - Display precision (default 4)
|
||||||
|
*/
|
||||||
|
export const formatTokenBalance = (
|
||||||
|
balance: string | number | bigint,
|
||||||
|
decimals: number,
|
||||||
|
precision: number = 4
|
||||||
|
): string => {
|
||||||
|
const balanceBigInt = BigInt(balance);
|
||||||
|
const divisor = BigInt(10 ** decimals);
|
||||||
|
const integerPart = balanceBigInt / divisor;
|
||||||
|
const fractionalPart = balanceBigInt % divisor;
|
||||||
|
|
||||||
|
const fractionalStr = fractionalPart.toString().padStart(decimals, '0');
|
||||||
|
const displayFractional = fractionalStr.slice(0, precision);
|
||||||
|
|
||||||
|
return `${integerPart}.${displayFractional}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse user input to raw balance
|
||||||
|
* @param input - User input string (e.g., "10.5")
|
||||||
|
* @param decimals - Token decimals
|
||||||
|
*/
|
||||||
|
export const parseTokenInput = (input: string, decimals: number): string => {
|
||||||
|
if (!input || input === '' || input === '.') return '0';
|
||||||
|
|
||||||
|
// Remove non-numeric chars except decimal point
|
||||||
|
const cleaned = input.replace(/[^\d.]/g, '');
|
||||||
|
const [integer, fractional] = cleaned.split('.');
|
||||||
|
|
||||||
|
const integerPart = BigInt(integer || '0') * BigInt(10 ** decimals);
|
||||||
|
|
||||||
|
if (!fractional) {
|
||||||
|
return integerPart.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad or truncate fractional part
|
||||||
|
const fractionalPadded = fractional.padEnd(decimals, '0').slice(0, decimals);
|
||||||
|
const fractionalPart = BigInt(fractionalPadded);
|
||||||
|
|
||||||
|
return (integerPart + fractionalPart).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate price impact for a swap
|
||||||
|
* @param reserveIn - Reserve of input token
|
||||||
|
* @param reserveOut - Reserve of output token
|
||||||
|
* @param amountIn - Amount being swapped in
|
||||||
|
*/
|
||||||
|
export const calculatePriceImpact = (
|
||||||
|
reserveIn: string,
|
||||||
|
reserveOut: string,
|
||||||
|
amountIn: string
|
||||||
|
): string => {
|
||||||
|
const reserveInBig = BigInt(reserveIn);
|
||||||
|
const reserveOutBig = BigInt(reserveOut);
|
||||||
|
const amountInBig = BigInt(amountIn);
|
||||||
|
|
||||||
|
if (reserveInBig === BigInt(0) || reserveOutBig === BigInt(0)) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price before = reserveOut / reserveIn
|
||||||
|
// Amount out with constant product: (amountIn * reserveOut) / (reserveIn + amountIn)
|
||||||
|
const amountOut =
|
||||||
|
(amountInBig * reserveOutBig) / (reserveInBig + amountInBig);
|
||||||
|
|
||||||
|
// Price after = (reserveOut - amountOut) / (reserveIn + amountIn)
|
||||||
|
const priceBefore = (reserveOutBig * BigInt(10000)) / reserveInBig;
|
||||||
|
const priceAfter =
|
||||||
|
((reserveOutBig - amountOut) * BigInt(10000)) /
|
||||||
|
(reserveInBig + amountInBig);
|
||||||
|
|
||||||
|
// Impact = |priceAfter - priceBefore| / priceBefore * 100
|
||||||
|
const impact = ((priceBefore - priceAfter) * BigInt(100)) / priceBefore;
|
||||||
|
|
||||||
|
return (Number(impact) / 100).toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate output amount for a swap (constant product formula)
|
||||||
|
* @param amountIn - Input amount
|
||||||
|
* @param reserveIn - Reserve of input token
|
||||||
|
* @param reserveOut - Reserve of output token
|
||||||
|
* @param feeRate - Fee rate (e.g., 30 for 0.3%)
|
||||||
|
*/
|
||||||
|
export const getAmountOut = (
|
||||||
|
amountIn: string,
|
||||||
|
reserveIn: string,
|
||||||
|
reserveOut: string,
|
||||||
|
feeRate: number = 30
|
||||||
|
): string => {
|
||||||
|
const amountInBig = BigInt(amountIn);
|
||||||
|
const reserveInBig = BigInt(reserveIn);
|
||||||
|
const reserveOutBig = BigInt(reserveOut);
|
||||||
|
|
||||||
|
if (
|
||||||
|
amountInBig === BigInt(0) ||
|
||||||
|
reserveInBig === BigInt(0) ||
|
||||||
|
reserveOutBig === BigInt(0)
|
||||||
|
) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// amountInWithFee = amountIn * (10000 - feeRate) / 10000
|
||||||
|
const amountInWithFee = (amountInBig * BigInt(10000 - feeRate)) / BigInt(10000);
|
||||||
|
|
||||||
|
// amountOut = (amountInWithFee * reserveOut) / (reserveIn + amountInWithFee)
|
||||||
|
const numerator = amountInWithFee * reserveOutBig;
|
||||||
|
const denominator = reserveInBig + amountInWithFee;
|
||||||
|
|
||||||
|
return (numerator / denominator).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate required amount1 for given amount2 (maintaining ratio)
|
||||||
|
* @param amount2 - Amount of token 2
|
||||||
|
* @param reserve1 - Reserve of token 1
|
||||||
|
* @param reserve2 - Reserve of token 2
|
||||||
|
*/
|
||||||
|
export const quote = (
|
||||||
|
amount2: string,
|
||||||
|
reserve1: string,
|
||||||
|
reserve2: string
|
||||||
|
): string => {
|
||||||
|
const amount2Big = BigInt(amount2);
|
||||||
|
const reserve1Big = BigInt(reserve1);
|
||||||
|
const reserve2Big = BigInt(reserve2);
|
||||||
|
|
||||||
|
if (reserve2Big === BigInt(0)) return '0';
|
||||||
|
|
||||||
|
return ((amount2Big * reserve1Big) / reserve2Big).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all existing pools from chain
|
||||||
|
* @param api - Polkadot API instance
|
||||||
|
*/
|
||||||
|
export const fetchPools = async (api: ApiPromise): Promise<PoolInfo[]> => {
|
||||||
|
try {
|
||||||
|
const pools: PoolInfo[] = [];
|
||||||
|
|
||||||
|
// Query all pool accounts
|
||||||
|
const poolKeys = await api.query.assetConversion.pools.keys();
|
||||||
|
|
||||||
|
for (const key of poolKeys) {
|
||||||
|
// Extract asset IDs from storage key
|
||||||
|
const [asset1Raw, asset2Raw] = key.args;
|
||||||
|
const asset1 = Number(asset1Raw.toString());
|
||||||
|
const asset2 = Number(asset2Raw.toString());
|
||||||
|
|
||||||
|
// Get pool account
|
||||||
|
const poolAccount = await api.query.assetConversion.pools([asset1, asset2]);
|
||||||
|
|
||||||
|
if (poolAccount.isNone) continue;
|
||||||
|
|
||||||
|
// Get reserves
|
||||||
|
const reserve1Data = await api.query.assets.account(asset1, poolAccount.unwrap());
|
||||||
|
const reserve2Data = await api.query.assets.account(asset2, poolAccount.unwrap());
|
||||||
|
|
||||||
|
const reserve1 = reserve1Data.isSome ? reserve1Data.unwrap().balance.toString() : '0';
|
||||||
|
const reserve2 = reserve2Data.isSome ? reserve2Data.unwrap().balance.toString() : '0';
|
||||||
|
|
||||||
|
// Get token info
|
||||||
|
const token1 = KNOWN_TOKENS[asset1] || {
|
||||||
|
id: asset1,
|
||||||
|
symbol: `Asset ${asset1}`,
|
||||||
|
name: `Unknown Asset ${asset1}`,
|
||||||
|
decimals: 12,
|
||||||
|
};
|
||||||
|
const token2 = KNOWN_TOKENS[asset2] || {
|
||||||
|
id: asset2,
|
||||||
|
symbol: `Asset ${asset2}`,
|
||||||
|
name: `Unknown Asset ${asset2}`,
|
||||||
|
decimals: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
pools.push({
|
||||||
|
id: `${asset1}-${asset2}`,
|
||||||
|
asset1,
|
||||||
|
asset2,
|
||||||
|
asset1Symbol: token1.symbol,
|
||||||
|
asset2Symbol: token2.symbol,
|
||||||
|
asset1Decimals: token1.decimals,
|
||||||
|
asset2Decimals: token2.decimals,
|
||||||
|
reserve1,
|
||||||
|
reserve2,
|
||||||
|
lpTokenSupply: '0', // TODO: Query LP token supply
|
||||||
|
feeRate: '0.3', // Default 0.3%
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pools;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch pools:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate amounts are greater than zero
|
||||||
|
*/
|
||||||
|
export const validateAmount = (amount: string): boolean => {
|
||||||
|
try {
|
||||||
|
const amountBig = BigInt(amount);
|
||||||
|
return amountBig > BigInt(0);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate minimum amount with slippage tolerance
|
||||||
|
* @param amount - Expected amount
|
||||||
|
* @param slippage - Slippage percentage (e.g., 1 for 1%)
|
||||||
|
*/
|
||||||
|
export const calculateMinAmount = (amount: string, slippage: number): string => {
|
||||||
|
const amountBig = BigInt(amount);
|
||||||
|
const slippageFactor = BigInt(10000 - slippage * 100);
|
||||||
|
return ((amountBig * slippageFactor) / BigInt(10000)).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get token symbol safely
|
||||||
|
*/
|
||||||
|
export const getTokenSymbol = (assetId: number): string => {
|
||||||
|
return KNOWN_TOKENS[assetId]?.symbol || `Asset ${assetId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get token decimals safely
|
||||||
|
*/
|
||||||
|
export const getTokenDecimals = (assetId: number): number => {
|
||||||
|
return KNOWN_TOKENS[assetId]?.decimals || 12;
|
||||||
|
};
|
||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"@pezkuwi/i18n": ["../shared/i18n"]
|
||||||
},
|
},
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const api = await ApiPromise.create({
|
|
||||||
provider: new WsProvider('ws://127.0.0.1:9944')
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n🔍 Checking PEZ/wUSDT Pool State...\n');
|
|
||||||
|
|
||||||
// Check if pool exists
|
|
||||||
const pool = await api.query.assetConversion.pools([1, 2]);
|
|
||||||
console.log('Pool exists:', pool.isSome);
|
|
||||||
|
|
||||||
if (pool.isSome) {
|
|
||||||
console.log('Pool data:', pool.unwrap().toHuman());
|
|
||||||
|
|
||||||
// Get pool reserves
|
|
||||||
const reserves = await api.query.assetConversion.poolReserves([1, 2]);
|
|
||||||
console.log('\nPool reserves:', reserves.toHuman());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check assets exist
|
|
||||||
const asset1 = await api.query.assets.asset(1);
|
|
||||||
const asset2 = await api.query.assets.asset(2);
|
|
||||||
console.log('\nPEZ (asset 1) exists:', asset1.isSome);
|
|
||||||
console.log('wUSDT (asset 2) exists:', asset2.isSome);
|
|
||||||
|
|
||||||
if (asset1.isSome) {
|
|
||||||
console.log('PEZ metadata:', (await api.query.assets.metadata(1)).toHuman());
|
|
||||||
}
|
|
||||||
if (asset2.isSome) {
|
|
||||||
console.log('wUSDT metadata:', (await api.query.assets.metadata(2)).toHuman());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all existing pools
|
|
||||||
console.log('\n📊 Checking all pools...');
|
|
||||||
const allPoolIds = [
|
|
||||||
[0, 1],
|
|
||||||
[0, 2],
|
|
||||||
[1, 2]
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const poolId of allPoolIds) {
|
|
||||||
const poolExists = await api.query.assetConversion.pools(poolId);
|
|
||||||
if (poolExists.isSome) {
|
|
||||||
const reserves = await api.query.assetConversion.poolReserves(poolId);
|
|
||||||
console.log(`\nPool [${poolId}]:`, poolExists.unwrap().toHuman());
|
|
||||||
console.log(` Reserves:`, reserves.toHuman());
|
|
||||||
} else {
|
|
||||||
console.log(`\nPool [${poolId}]: Does not exist`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
+10
-1
@@ -6,7 +6,15 @@ import path from "path";
|
|||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
server: {
|
server: {
|
||||||
host: "::",
|
host: "::",
|
||||||
port: 8080,
|
port: 8081,
|
||||||
|
hmr: {
|
||||||
|
protocol: 'ws',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8081,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
react()
|
react()
|
||||||
@@ -14,6 +22,7 @@ export default defineConfig(({ mode }) => ({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
"@pezkuwi/i18n": path.resolve(__dirname, "../shared/i18n"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
|
||||||
import { Keyring } from '@polkadot/keyring';
|
|
||||||
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await cryptoWaitReady();
|
|
||||||
const wsProvider = new WsProvider('ws://127.0.0.1:9944');
|
|
||||||
const api = await ApiPromise.create({ provider: wsProvider });
|
|
||||||
const keyring = new Keyring({ type: 'sr25519' });
|
|
||||||
const founder = keyring.addFromUri('skill dose toward always latin fish film cabbage praise blouse kingdom depth');
|
|
||||||
|
|
||||||
console.log('\n🔄 Wrapping 200,000 HEZ to wHEZ...');
|
|
||||||
const wrapAmount = BigInt(200_000) * BigInt(10 ** 12);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.tokenWrapper
|
|
||||||
.wrap(wrapAmount.toString())
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
if (dispatchError.isModule) {
|
|
||||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
|
||||||
console.log(' Error:', decoded.name, '-', decoded.docs.join(' '));
|
|
||||||
} else {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
}
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'tokenWrapper') {
|
|
||||||
console.log(' Event:', event.method, event.data.toHuman());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(' ✓ Wrapped 200,000 HEZ to wHEZ');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n🚀 Creating all 3 pools...\n');
|
|
||||||
console.log('Target exchange rates:');
|
|
||||||
console.log(' 1 wUSDT = 4 wHEZ = 20 PEZ\n');
|
|
||||||
|
|
||||||
// Pool 1: wHEZ/PEZ (1 wHEZ = 5 PEZ)
|
|
||||||
console.log('📝 Pool 1: wHEZ/PEZ');
|
|
||||||
console.log(' Ratio: 100,000 wHEZ : 500,000 PEZ (1:5)');
|
|
||||||
const whezPez_whez = BigInt(100_000) * BigInt(10 ** 12);
|
|
||||||
const whezPez_pez = BigInt(500_000) * BigInt(10 ** 12);
|
|
||||||
|
|
||||||
const pool1 = await api.query.assetConversion.pools([0, 1]);
|
|
||||||
if (pool1.isNone) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(0, 1)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create error:', dispatchError.toString());
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
0, 1,
|
|
||||||
whezPez_whez.toString(),
|
|
||||||
whezPez_pez.toString(),
|
|
||||||
whezPez_whez.toString(),
|
|
||||||
whezPez_pez.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
if (dispatchError.isModule) {
|
|
||||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
|
||||||
console.log(' Error:', decoded.name, '-', decoded.docs.join(' '));
|
|
||||||
} else {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
}
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to wHEZ/PEZ pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pool 2: wHEZ/wUSDT (4:1)
|
|
||||||
console.log('\n📝 Pool 2: wHEZ/wUSDT');
|
|
||||||
console.log(' Ratio: 40,000 wHEZ : 10,000 wUSDT (4:1)');
|
|
||||||
const whezUsdt_whez = BigInt(40_000) * BigInt(10 ** 12);
|
|
||||||
const whezUsdt_usdt = BigInt(10_000) * BigInt(10 ** 6);
|
|
||||||
|
|
||||||
const pool2 = await api.query.assetConversion.pools([0, 2]);
|
|
||||||
if (pool2.isNone) {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.createPool(0, 2)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
console.log(' Create error:', dispatchError.toString());
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool created');
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(' ✓ Pool already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Adding liquidity...');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
api.tx.assetConversion
|
|
||||||
.addLiquidity(
|
|
||||||
0, 2,
|
|
||||||
whezUsdt_whez.toString(),
|
|
||||||
whezUsdt_usdt.toString(),
|
|
||||||
whezUsdt_whez.toString(),
|
|
||||||
whezUsdt_usdt.toString(),
|
|
||||||
founder.address
|
|
||||||
)
|
|
||||||
.signAndSend(founder, ({ status, dispatchError, events }) => {
|
|
||||||
if (status.isInBlock) {
|
|
||||||
if (dispatchError) {
|
|
||||||
if (dispatchError.isModule) {
|
|
||||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
|
||||||
console.log(' Error:', decoded.name, '-', decoded.docs.join(' '));
|
|
||||||
} else {
|
|
||||||
console.log(' Error:', dispatchError.toString());
|
|
||||||
}
|
|
||||||
reject(new Error(dispatchError.toString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events.forEach(({ event }) => {
|
|
||||||
if (event.section === 'assetConversion' && event.method === 'LiquidityAdded') {
|
|
||||||
console.log(' ✓ Liquidity added to wHEZ/wUSDT pool');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pool 3: PEZ/wUSDT (20:1) - already exists but add more liquidity
|
|
||||||
console.log('\n📝 Pool 3: PEZ/wUSDT');
|
|
||||||
console.log(' Pool already exists with liquidity');
|
|
||||||
|
|
||||||
console.log('\n✅ All 3 pools are ready!');
|
|
||||||
console.log('\nPool Summary:');
|
|
||||||
console.log(' 1. wHEZ/PEZ: 100k:500k (1 wHEZ = 5 PEZ)');
|
|
||||||
console.log(' 2. wHEZ/wUSDT: 40k:10k (4 wHEZ = 1 wUSDT)');
|
|
||||||
console.log(' 3. PEZ/wUSDT: 200k:10k (20 PEZ = 1 wUSDT)');
|
|
||||||
console.log('\nExchange rates: 1 wUSDT = 4 wHEZ = 20 PEZ ✓');
|
|
||||||
|
|
||||||
await api.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
Reference in New Issue
Block a user