import { computed, ref } from 'vue'; import { useAccount } from '@wagmi/vue'; import { readContract, writeContract } from '@wagmi/core'; import { erc20Abi, maxUint256, parseEther, parseUnits, zeroAddress, type Abi } from 'viem'; import { useToast } from 'vue-toastification'; import { config as wagmiConfig } from '@/wagmi'; import { getChain, DEFAULT_CHAIN_ID } from '@/config'; import { useWallet } from '@/composables/useWallet'; import { getErrorMessage, ensureAddress } from '@harb/utils'; export const WETH_ABI = [ { inputs: [], name: 'deposit', outputs: [], stateMutability: 'payable', type: 'function', }, ] as const satisfies Abi; export const SWAP_ROUTER_ABI = [ { inputs: [], name: 'factory', outputs: [{ internalType: 'address', name: '', type: 'address' }], stateMutability: 'view', type: 'function', }, { inputs: [ { components: [ { internalType: 'address', name: 'tokenIn', type: 'address' }, { internalType: 'address', name: 'tokenOut', type: 'address' }, { internalType: 'uint24', name: 'fee', type: 'uint24' }, { internalType: 'address', name: 'recipient', type: 'address' }, { internalType: 'uint256', name: 'amountIn', type: 'uint256' }, { internalType: 'uint256', name: 'amountOutMinimum', type: 'uint256' }, { internalType: 'uint160', name: 'sqrtPriceLimitX96', type: 'uint160' }, ], internalType: 'struct ISwapRouter.ExactInputSingleParams', name: 'params', type: 'tuple', }, ], name: 'exactInputSingle', outputs: [{ internalType: 'uint256', name: 'amountOut', type: 'uint256' }], stateMutability: 'payable', type: 'function', }, ] as const satisfies Abi; export const UNISWAP_FACTORY_ABI = [ { inputs: [ { internalType: 'address', name: 'tokenA', type: 'address' }, { internalType: 'address', name: 'tokenB', type: 'address' }, { internalType: 'uint24', name: 'fee', type: 'uint24' }, ], name: 'getPool', outputs: [{ internalType: 'address', name: 'pool', type: 'address' }], stateMutability: 'view', type: 'function', }, ] as const satisfies Abi; // Full pool ABI used by both the swap flow (liquidity check) and liquidity stats (slot0). export const UNISWAP_POOL_ABI = [ { inputs: [], name: 'slot0', outputs: [ { internalType: 'uint160', name: 'sqrtPriceX96', type: 'uint160' }, { internalType: 'int24', name: 'tick', type: 'int24' }, { internalType: 'uint16', name: 'observationIndex', type: 'uint16' }, { internalType: 'uint16', name: 'observationCardinality', type: 'uint16' }, { internalType: 'uint16', name: 'observationCardinalityNext', type: 'uint16' }, { internalType: 'uint8', name: 'feeProtocol', type: 'uint8' }, { internalType: 'bool', name: 'unlocked', type: 'bool' }, ], stateMutability: 'view', type: 'function', }, { inputs: [], name: 'liquidity', outputs: [{ internalType: 'uint128', name: '', type: 'uint128' }], stateMutability: 'view', type: 'function', }, ] as const satisfies Abi; export function useSwapKrk() { const toast = useToast(); const { address, chainId } = useAccount(); const { loadBalance } = useWallet(); const swapAmount = ref('0.1'); const swapping = ref(false); const sellAmount = ref(''); const selling = ref(false); const sellPhase = ref<'approving' | 'selling'>('approving'); const krkBalance = ref(0n); const resolvedChainId = computed(() => chainId.value ?? DEFAULT_CHAIN_ID); const chainConfig = computed(() => getChain(resolvedChainId.value)); const cheatConfig = computed(() => chainConfig.value?.cheats ?? null); const canSwap = computed(() => Boolean(address.value && cheatConfig.value?.weth && cheatConfig.value?.swapRouter && chainConfig.value?.harb) ); async function loadKrkBalance() { if (!address.value || !chainConfig.value?.harb) return; try { const harb = ensureAddress(chainConfig.value.harb, 'KRAIKEN token'); const caller = ensureAddress(address.value, 'Wallet address'); krkBalance.value = await readContract(wagmiConfig, { abi: erc20Abi, address: harb, functionName: 'balanceOf', args: [caller], chainId: resolvedChainId.value, }); } catch { // Do not overwrite a previously loaded non-zero balance on RPC error } } async function buyKrk() { if (!canSwap.value || swapping.value) return; try { swapping.value = true; const chain = chainConfig.value; const cheat = cheatConfig.value; if (!chain?.harb) throw new Error('No Kraiken deployment configured for this chain'); if (!cheat) throw new Error('Swap helpers are not configured for this chain'); const weth = ensureAddress(cheat.weth, 'WETH'); const router = ensureAddress(cheat.swapRouter, 'Swap router'); const harb = ensureAddress(chain.harb, 'KRAIKEN token'); const caller = ensureAddress(address.value!, 'Wallet address'); const currentChainId = resolvedChainId.value; let amount: bigint; try { amount = parseEther(swapAmount.value || '0'); } catch { throw new Error('Enter a valid ETH amount'); } if (amount <= 0n) throw new Error('Amount must be greater than zero'); const factoryAddress = await readContract(wagmiConfig, { abi: SWAP_ROUTER_ABI, address: router, functionName: 'factory', chainId: currentChainId, }); const factory = ensureAddress(factoryAddress, 'Uniswap factory'); const poolAddress = await readContract(wagmiConfig, { abi: UNISWAP_FACTORY_ABI, address: factory, functionName: 'getPool', args: [weth, harb, 10_000], chainId: currentChainId, }); if (!poolAddress || poolAddress === zeroAddress) { throw new Error('No KRK/WETH pool found at 1% fee; deploy and recenter first'); } const poolLiquidity = await readContract(wagmiConfig, { abi: UNISWAP_POOL_ABI, address: poolAddress, functionName: 'liquidity', chainId: currentChainId, }); if (poolLiquidity === 0n) { throw new Error('KRK/WETH pool has zero liquidity; run recenter before swapping'); } const wethBalance = await readContract(wagmiConfig, { abi: erc20Abi, address: weth, functionName: 'balanceOf', args: [caller], chainId: currentChainId, }); const wrapAmount = amount > wethBalance ? amount - wethBalance : 0n; if (wrapAmount > 0n) { await writeContract(wagmiConfig, { abi: WETH_ABI, address: weth, functionName: 'deposit', value: wrapAmount, chainId: currentChainId, }); } const allowance = await readContract(wagmiConfig, { abi: erc20Abi, address: weth, functionName: 'allowance', args: [caller, router], chainId: currentChainId, }); if (allowance < amount) { await writeContract(wagmiConfig, { abi: erc20Abi, address: weth, functionName: 'approve', args: [router, maxUint256], chainId: currentChainId, }); } await writeContract(wagmiConfig, { abi: SWAP_ROUTER_ABI, address: router, functionName: 'exactInputSingle', args: [ { tokenIn: weth, tokenOut: harb, fee: 10_000, recipient: caller, amountIn: amount, amountOutMinimum: 0n, // no slippage protection — acceptable on no-MEV local anvil sqrtPriceLimitX96: 0n, }, ], chainId: currentChainId, }); await loadBalance(); toast.success('Swap complete. $KRK has been added to your wallet.'); } catch (error: unknown) { toast.error(getErrorMessage(error, 'Swap failed')); } finally { swapping.value = false; } } async function sellKrk() { if (!canSwap.value || selling.value) return; try { selling.value = true; const chain = chainConfig.value; const cheat = cheatConfig.value; if (!chain?.harb) throw new Error('No Kraiken deployment configured for this chain'); if (!cheat) throw new Error('Swap helpers are not configured for this chain'); const weth = ensureAddress(cheat.weth, 'WETH'); const router = ensureAddress(cheat.swapRouter, 'Swap router'); const harb = ensureAddress(chain.harb, 'KRAIKEN token'); const caller = ensureAddress(address.value!, 'Wallet address'); const currentChainId = resolvedChainId.value; let amount: bigint; try { amount = parseUnits(sellAmount.value || '0', 18); } catch { throw new Error('Enter a valid KRK amount'); } if (amount <= 0n) throw new Error('Amount must be greater than zero'); const balance = await readContract(wagmiConfig, { abi: erc20Abi, address: harb, functionName: 'balanceOf', args: [caller], chainId: currentChainId, }); if (amount > balance) throw new Error('Insufficient KRK balance'); const factoryAddress = await readContract(wagmiConfig, { abi: SWAP_ROUTER_ABI, address: router, functionName: 'factory', chainId: currentChainId, }); const factory = ensureAddress(factoryAddress, 'Uniswap factory'); const poolAddress = await readContract(wagmiConfig, { abi: UNISWAP_FACTORY_ABI, address: factory, functionName: 'getPool', args: [weth, harb, 10_000], chainId: currentChainId, }); if (!poolAddress || poolAddress === zeroAddress) { throw new Error('No KRK/WETH pool found at 1% fee; deploy and recenter first'); } const poolLiquidity = await readContract(wagmiConfig, { abi: UNISWAP_POOL_ABI, address: poolAddress, functionName: 'liquidity', chainId: currentChainId, }); if (poolLiquidity === 0n) { throw new Error('KRK/WETH pool has zero liquidity; run recenter before swapping'); } sellPhase.value = 'approving'; const allowance = await readContract(wagmiConfig, { abi: erc20Abi, address: harb, functionName: 'allowance', args: [caller, router], chainId: currentChainId, }); if (allowance < amount) { await writeContract(wagmiConfig, { abi: erc20Abi, address: harb, functionName: 'approve', args: [router, maxUint256], chainId: currentChainId, }); } sellPhase.value = 'selling'; await writeContract(wagmiConfig, { abi: SWAP_ROUTER_ABI, address: router, functionName: 'exactInputSingle', args: [ { tokenIn: harb, tokenOut: weth, fee: 10_000, recipient: caller, amountIn: amount, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n, }, ], chainId: currentChainId, }); await loadBalance(); await loadKrkBalance(); sellAmount.value = ''; toast.success('Sell complete. WETH has been added to your wallet.'); } catch (error: unknown) { toast.error(getErrorMessage(error, 'Sell failed')); } finally { selling.value = false; sellPhase.value = 'approving'; } } return { swapAmount, swapping, canSwap, buyKrk, sellAmount, selling, sellPhase, krkBalance, loadKrkBalance, sellKrk }; }