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:
parent
59d8364ef3
commit
e5a5486499
3 changed files with 275 additions and 487 deletions
|
|
@ -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>
|
||||
|
|
|
|||
260
web-app/src/composables/useSwapKrk.ts
Normal file
260
web-app/src/composables/useSwapKrk.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
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';
|
||||
|
||||
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 isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
export function coerceString(value: unknown): string | null {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length > 0) return trimmed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export function ensureAddress(value: string, label: string): Address {
|
||||
if (!value || !isAddress(value)) {
|
||||
throw new Error(`${label} is not a valid address`);
|
||||
}
|
||||
return getAddress(value);
|
||||
}
|
||||
|
||||
export function 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)
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return { swapAmount, swapping, canSwap, buyKrk };
|
||||
}
|
||||
|
|
@ -147,20 +147,16 @@ import FCard from '@/components/fcomponents/FCard.vue';
|
|||
import FInput from '@/components/fcomponents/FInput.vue';
|
||||
import { config } from '@/wagmi';
|
||||
import { useStatCollection } from '@/composables/useStatCollection';
|
||||
import { useWallet } from '@/composables/useWallet';
|
||||
import { useToast } from 'vue-toastification';
|
||||
import { useAccount } from '@wagmi/vue';
|
||||
import { getChain, DEFAULT_CHAIN_ID } from '@/config';
|
||||
import { readContract, writeContract, type Config as WagmiConfig } from '@wagmi/core';
|
||||
import { readContract, type Config as WagmiConfig } from '@wagmi/core';
|
||||
import HarbJson from '@/assets/contracts/harb.json';
|
||||
import {
|
||||
createPublicClient,
|
||||
erc20Abi,
|
||||
formatEther,
|
||||
getAddress,
|
||||
http,
|
||||
isAddress,
|
||||
maxUint256,
|
||||
parseEther,
|
||||
toHex,
|
||||
zeroAddress,
|
||||
|
|
@ -168,13 +164,24 @@ import {
|
|||
type Abi,
|
||||
type EIP1193Provider,
|
||||
} from 'viem';
|
||||
import {
|
||||
SWAP_ROUTER_ABI,
|
||||
UNISWAP_FACTORY_ABI,
|
||||
UNISWAP_POOL_ABI,
|
||||
isRecord,
|
||||
coerceString,
|
||||
getErrorMessage,
|
||||
ensureAddress,
|
||||
useSwapKrk,
|
||||
} from '@/composables/useSwapKrk';
|
||||
|
||||
const toast = useToast();
|
||||
const statCollection = useStatCollection();
|
||||
const { address, chainId } = useAccount();
|
||||
const { loadBalance } = useWallet();
|
||||
const wagmiConfig: WagmiConfig = config;
|
||||
|
||||
const { swapAmount, swapping, canSwap, buyKrk } = useSwapKrk();
|
||||
|
||||
const rpcUrl = ref('');
|
||||
let lastAutoRpcUrl = '';
|
||||
|
||||
|
|
@ -182,9 +189,6 @@ const mintTarget = ref('');
|
|||
const mintAmount = ref('10');
|
||||
const minting = ref(false);
|
||||
|
||||
const swapAmount = ref('0.1');
|
||||
const swapping = ref(false);
|
||||
|
||||
const forcing = ref(false);
|
||||
|
||||
const addingNetwork = ref(false);
|
||||
|
|
@ -249,9 +253,6 @@ 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 hasWalletProvider = computed(() => typeof window !== 'undefined' && Boolean(window.ethereum?.request));
|
||||
const canAddToken = computed(() => Boolean(hasWalletProvider.value && chainConfig.value?.harb));
|
||||
const canAddWeth = computed(() => Boolean(hasWalletProvider.value && cheatConfig.value?.weth));
|
||||
|
|
@ -331,40 +332,6 @@ function getEthereumProvider(): EIP1193Provider {
|
|||
return window.ethereum;
|
||||
}
|
||||
|
||||
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 message = coerceString(error.message);
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
if (isRecord(error)) {
|
||||
const shortMessage = coerceString(error.shortMessage);
|
||||
if (shortMessage) {
|
||||
return shortMessage;
|
||||
}
|
||||
const message = coerceString(error.message);
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function extractMessageFromPayload(payload: unknown, keys: string[], fallback: string): string {
|
||||
if (!isRecord(payload)) {
|
||||
return fallback;
|
||||
|
|
@ -378,87 +345,6 @@ function extractMessageFromPayload(payload: unknown, keys: string[], fallback: s
|
|||
return fallback;
|
||||
}
|
||||
|
||||
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: '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;
|
||||
|
||||
const LIQUIDITY_MANAGER_POSITIONS_ABI = [
|
||||
{
|
||||
inputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
|
||||
|
|
@ -798,13 +684,6 @@ async function rpcRequest<T>(method: string, params: unknown[]): Promise<T> {
|
|||
throw new Error('RPC response missing result field');
|
||||
}
|
||||
|
||||
function ensureAddress(value: string, label: string): Address {
|
||||
if (!value || !isAddress(value)) {
|
||||
throw new Error(`${label} is not a valid address`);
|
||||
}
|
||||
return getAddress(value);
|
||||
}
|
||||
|
||||
async function mintEth() {
|
||||
if (minting.value) return;
|
||||
try {
|
||||
|
|
@ -954,120 +833,6 @@ async function addWethToken() {
|
|||
}
|
||||
}
|
||||
|
||||
async function buyKrk() {
|
||||
if (!canSwap.value || swapping.value) return;
|
||||
try {
|
||||
swapping.value = true;
|
||||
if (!chainConfig.value?.harb) {
|
||||
throw new Error('No Kraiken deployment configured for this chain');
|
||||
}
|
||||
if (!cheatConfig.value) {
|
||||
throw new Error('Swap helpers are not configured for this chain');
|
||||
}
|
||||
const weth = ensureAddress(cheatConfig.value!.weth, 'WETH');
|
||||
const router = ensureAddress(cheatConfig.value!.swapRouter, 'Swap router');
|
||||
const harb = ensureAddress(chainConfig.value!.harb, 'KRAIKEN token');
|
||||
const caller = ensureAddress(address.value!, 'Wallet address');
|
||||
const chainId = 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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
const wrapAmount = amount > wethBalance ? amount - wethBalance : 0n;
|
||||
|
||||
if (wrapAmount > 0n) {
|
||||
await writeContract(wagmiConfig, {
|
||||
abi: WETH_ABI,
|
||||
address: weth,
|
||||
functionName: 'deposit',
|
||||
value: wrapAmount,
|
||||
chainId,
|
||||
});
|
||||
}
|
||||
const allowance = await readContract(wagmiConfig, {
|
||||
abi: erc20Abi,
|
||||
address: weth,
|
||||
functionName: 'allowance',
|
||||
args: [caller, router],
|
||||
chainId,
|
||||
});
|
||||
|
||||
if (allowance < amount) {
|
||||
await writeContract(wagmiConfig, {
|
||||
abi: erc20Abi,
|
||||
address: weth,
|
||||
functionName: 'approve',
|
||||
args: [router, maxUint256],
|
||||
chainId,
|
||||
});
|
||||
}
|
||||
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,
|
||||
sqrtPriceLimitX96: 0n,
|
||||
},
|
||||
],
|
||||
chainId,
|
||||
});
|
||||
|
||||
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 forceRecenter() {
|
||||
if (forcing.value || !txnBotRecenterUrl.value) return;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue