fix: Structural duplication of swap ABIs and buyKrk() across LocalSwapWidget and CheatsView (#353)

Extract shared Uniswap ABIs (WETH_ABI, SWAP_ROUTER_ABI, UNISWAP_FACTORY_ABI,
UNISWAP_POOL_ABI), utility functions (isRecord, coerceString, getErrorMessage,
ensureAddress), and the wrap→approve→exactInputSingle flow into a new composable
useSwapKrk(). Both LocalSwapWidget and CheatsView now delegate to this single
source of truth, so swap logic changes only need to be made in one place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-28 09:32:02 +00:00
parent 59d8364ef3
commit e5a5486499
3 changed files with 275 additions and 487 deletions

View file

@ -17,246 +17,9 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useAccount } from '@wagmi/vue';
import { readContract, writeContract } from '@wagmi/core';
import { erc20Abi, getAddress, isAddress, maxUint256, parseEther, zeroAddress, type Abi, type Address } 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 { useSwapKrk } from '@/composables/useSwapKrk';
const toast = useToast();
const { address, chainId } = useAccount();
const { loadBalance } = useWallet();
const swapAmount = ref('0.1');
const swapping = ref(false);
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)
);
const WETH_ABI = [
{
inputs: [],
name: 'deposit',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
] as const satisfies Abi;
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;
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;
const UNISWAP_POOL_ABI = [
{
inputs: [],
name: 'liquidity',
outputs: [{ internalType: 'uint128', name: '', type: 'uint128' }],
stateMutability: 'view',
type: 'function',
},
] as const satisfies Abi;
function ensureAddress(value: string, label: string): Address {
if (!value || !isAddress(value)) {
throw new Error(`${label} is not a valid address`);
}
return getAddress(value);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function coerceString(value: unknown): string | null {
if (typeof value === 'string') {
const trimmed = value.trim();
if (trimmed.length > 0) return trimmed;
}
return null;
}
function getErrorMessage(error: unknown, fallback: string): string {
if (error instanceof Error) {
const msg = coerceString(error.message);
if (msg) return msg;
}
if (isRecord(error)) {
const short = coerceString(error.shortMessage);
if (short) return short;
const msg = coerceString(error.message);
if (msg) return msg;
}
return fallback;
}
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;
}
}
const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
</script>
<style lang="sass" scoped>