2026-02-28 09:32:02 +00:00
|
|
|
import { computed, ref } from 'vue';
|
|
|
|
|
import { useAccount } from '@wagmi/vue';
|
|
|
|
|
import { readContract, writeContract } from '@wagmi/core';
|
2026-03-05 12:03:36 +00:00
|
|
|
import { erc20Abi, maxUint256, parseEther, parseUnits, zeroAddress, type Abi } from 'viem';
|
2026-02-28 09:32:02 +00:00
|
|
|
import { useToast } from 'vue-toastification';
|
|
|
|
|
import { config as wagmiConfig } from '@/wagmi';
|
|
|
|
|
import { getChain, DEFAULT_CHAIN_ID } from '@/config';
|
|
|
|
|
import { useWallet } from '@/composables/useWallet';
|
2026-03-03 05:37:14 +00:00
|
|
|
import { getErrorMessage, ensureAddress } from '@harb/utils';
|
2026-02-28 09:32:02 +00:00
|
|
|
|
|
|
|
|
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);
|
2026-03-05 12:03:36 +00:00
|
|
|
const sellAmount = ref('');
|
|
|
|
|
const selling = ref(false);
|
|
|
|
|
const sellPhase = ref<'approving' | 'selling'>('approving');
|
|
|
|
|
const krkBalance = ref<bigint>(0n);
|
2026-02-28 09:32:02 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-05 12:03:36 +00:00
|
|
|
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 {
|
2026-03-05 12:39:15 +00:00
|
|
|
// Do not overwrite a previously loaded non-zero balance on RPC error
|
2026-03-05 12:03:36 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 09:32:02 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 12:03:36 +00:00
|
|
|
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');
|
|
|
|
|
|
2026-03-05 12:39:15 +00:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 12:03:36 +00:00
|
|
|
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();
|
2026-03-05 12:39:15 +00:00
|
|
|
sellAmount.value = '';
|
2026-03-05 12:03:36 +00:00
|
|
|
|
|
|
|
|
toast.success('Sell complete. WETH has been added to your wallet.');
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
toast.error(getErrorMessage(error, 'Sell failed'));
|
|
|
|
|
} finally {
|
|
|
|
|
selling.value = false;
|
2026-03-05 12:39:15 +00:00
|
|
|
sellPhase.value = 'approving';
|
2026-03-05 12:03:36 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { swapAmount, swapping, canSwap, buyKrk, sellAmount, selling, sellPhase, krkBalance, loadKrkBalance, sellKrk };
|
2026-02-28 09:32:02 +00:00
|
|
|
}
|