429 lines
14 KiB
Vue
429 lines
14 KiB
Vue
<template>
|
|
<div class="cheats-view">
|
|
<div class="cheats-header">
|
|
<h2>Cheat Console</h2>
|
|
<p class="cheats-caption">
|
|
Helpers for local forks while we wait on live liquidity. Requests hit the configured RPC directly, so keep this pointed at a trusted node only.
|
|
</p>
|
|
</div>
|
|
|
|
<f-card>
|
|
<div class="cheats-form">
|
|
<f-input v-model="rpcUrl" label="RPC URL"></f-input>
|
|
<p class="cheats-hint">Defaults to the Anvil instance from <code>scripts/local_env.sh</code>.</p>
|
|
<div class="cheats-actions">
|
|
<f-button
|
|
:disabled="!hasWalletProvider || addingNetwork"
|
|
@click="addNetwork"
|
|
>{{ addingNetwork ? "Adding…" : "Add network to wallet" }}</f-button>
|
|
<f-button
|
|
:disabled="!canAddToken || addingToken"
|
|
@click="addToken"
|
|
variant="secondary"
|
|
>{{ addingToken ? "Adding…" : "Add KRK token" }}</f-button>
|
|
</div>
|
|
</div>
|
|
</f-card>
|
|
|
|
<div class="cheats-grid">
|
|
<f-card title="Mint ETH">
|
|
<div class="cheats-form">
|
|
<f-input v-model="mintTarget" label="Recipient"></f-input>
|
|
<f-input v-model="mintAmount" label="Additional ETH" type="number">
|
|
<template #details>Amount to add on top of the current balance.</template>
|
|
</f-input>
|
|
<f-button :disabled="minting" @click="mintEth">{{ minting ? "Minting…" : "Mint" }}</f-button>
|
|
</div>
|
|
</f-card>
|
|
|
|
<f-card title="Buy KRK">
|
|
<div class="cheats-form">
|
|
<template v-if="!canSwap">
|
|
<p class="cheats-warning">Connect to the Base Sepolia fork to enable the quick swap helper.</p>
|
|
</template>
|
|
<template v-else>
|
|
<f-input v-model="swapAmount" label="ETH to spend" type="number"></f-input>
|
|
<f-button :disabled="swapping" @click="buyKrk">{{ swapping ? "Submitting…" : "Buy" }}</f-button>
|
|
<p class="cheats-hint">Wraps ETH into WETH, approves the swap router, and trades into KRK.</p>
|
|
</template>
|
|
</div>
|
|
</f-card>
|
|
|
|
<f-card title="Force Recenter">
|
|
<div class="cheats-form">
|
|
<template v-if="!canTriggerRecenter">
|
|
<p class="cheats-warning">No txnBot recenter endpoint detected for this chain.</p>
|
|
</template>
|
|
<template v-else>
|
|
<f-button :disabled="forcing" @click="forceRecenter">{{ forcing ? "Calling…" : "Call recenter" }}</f-button>
|
|
<p class="cheats-hint">Requests the local txnBot to execute <code>recenter()</code> with its operator key.</p>
|
|
</template>
|
|
</div>
|
|
</f-card>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch } from "vue";
|
|
import FButton from "@/components/fcomponents/FButton.vue";
|
|
import FCard from "@/components/fcomponents/FCard.vue";
|
|
import FInput from "@/components/fcomponents/FInput.vue";
|
|
import { config } from "@/wagmi";
|
|
import { useToast } from "vue-toastification";
|
|
import { useAccount } from "@wagmi/vue";
|
|
import { getChain, DEFAULT_CHAIN_ID } from "@/config";
|
|
import { readContract, writeContract } from "@wagmi/core";
|
|
import {
|
|
erc20Abi,
|
|
getAddress,
|
|
isAddress,
|
|
parseEther,
|
|
toHex,
|
|
type Address
|
|
} from "viem";
|
|
|
|
const toast = useToast();
|
|
const { address, chainId } = useAccount();
|
|
|
|
const rpcUrl = ref("http://127.0.0.1:8545");
|
|
|
|
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);
|
|
const addingToken = ref(false);
|
|
|
|
watch(address, (value) => {
|
|
if (value) {
|
|
mintTarget.value = value;
|
|
}
|
|
}, { immediate: true });
|
|
|
|
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 as any)?.ethereum?.request));
|
|
const canAddToken = computed(() => Boolean(hasWalletProvider.value && chainConfig.value?.harb));
|
|
const txnBotRecenterUrl = computed(() => {
|
|
const baseUrl = cheatConfig.value?.txnBot?.trim();
|
|
if (!baseUrl) return null;
|
|
return `${baseUrl.replace(/\/$/, "")}/recenter`;
|
|
});
|
|
const canTriggerRecenter = computed(() => Boolean(txnBotRecenterUrl.value));
|
|
|
|
function resolveChainName(chainId: number): string {
|
|
switch (chainId) {
|
|
case 31337:
|
|
return "Local Anvil";
|
|
case 84532:
|
|
return "Base Sepolia";
|
|
case 8453:
|
|
return "Base";
|
|
case 11155111:
|
|
return "Ethereum Sepolia";
|
|
default:
|
|
return `Chain ${chainId}`;
|
|
}
|
|
}
|
|
|
|
const WETH_ABI = [
|
|
{
|
|
inputs: [],
|
|
name: "deposit",
|
|
outputs: [],
|
|
stateMutability: "payable",
|
|
type: "function",
|
|
},
|
|
];
|
|
|
|
const SWAP_ROUTER_ABI = [
|
|
{
|
|
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",
|
|
},
|
|
];
|
|
|
|
async function rpcRequest<T>(method: string, params: unknown[]): Promise<T> {
|
|
if (!rpcUrl.value) {
|
|
throw new Error("RPC URL required");
|
|
}
|
|
const response = await fetch(rpcUrl.value, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params }),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`RPC ${method} failed with status ${response.status}`);
|
|
}
|
|
const payload = await response.json();
|
|
if (payload.error) {
|
|
throw new Error(payload.error.message ?? "RPC error");
|
|
}
|
|
return payload.result as T;
|
|
}
|
|
|
|
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 {
|
|
minting.value = true;
|
|
const target = ensureAddress(mintTarget.value, "Recipient");
|
|
let amount: bigint;
|
|
try {
|
|
amount = parseEther(mintAmount.value || "0");
|
|
} catch {
|
|
throw new Error("Enter a valid ETH amount");
|
|
}
|
|
if (amount <= 0n) {
|
|
throw new Error("Amount must be greater than zero");
|
|
}
|
|
const current = await rpcRequest<string>("eth_getBalance", [target, "latest"]);
|
|
const nextBalance = BigInt(current ?? "0x0") + amount;
|
|
await rpcRequest("anvil_setBalance", [target, toHex(nextBalance)]);
|
|
toast.success(`Set balance to ${mintAmount.value} ETH higher for ${target}`);
|
|
} catch (error: any) {
|
|
toast.error(error?.message ?? "Mint failed");
|
|
} finally {
|
|
minting.value = false;
|
|
}
|
|
}
|
|
|
|
async function addNetwork() {
|
|
if (addingNetwork.value || !hasWalletProvider.value) return;
|
|
try {
|
|
addingNetwork.value = true;
|
|
if (!rpcUrl.value) {
|
|
throw new Error("Enter an RPC URL to add the network");
|
|
}
|
|
const chain = chainConfig.value;
|
|
if (!chain) {
|
|
throw new Error("Select a supported network first");
|
|
}
|
|
const provider = (window as any).ethereum;
|
|
await provider.request({
|
|
method: "wallet_addEthereumChain",
|
|
params: [
|
|
{
|
|
chainId: toHex(BigInt(resolvedChainId.value)),
|
|
chainName: resolveChainName(resolvedChainId.value),
|
|
rpcUrls: [rpcUrl.value],
|
|
nativeCurrency: {
|
|
name: "Ether",
|
|
symbol: "ETH",
|
|
decimals: 18,
|
|
},
|
|
blockExplorerUrls: [] as string[],
|
|
},
|
|
],
|
|
});
|
|
toast.success("Network request sent to your wallet");
|
|
} catch (error: any) {
|
|
toast.error(error?.message ?? "Failed to add network");
|
|
} finally {
|
|
addingNetwork.value = false;
|
|
}
|
|
}
|
|
|
|
async function addToken() {
|
|
if (addingToken.value || !canAddToken.value) return;
|
|
try {
|
|
addingToken.value = true;
|
|
const chain = chainConfig.value;
|
|
if (!chain?.harb) {
|
|
throw new Error("KRAIKEN token address not configured for this chain");
|
|
}
|
|
const harbAddress = ensureAddress(chain.harb, "KRAIKEN token");
|
|
const provider = (window as any).ethereum;
|
|
const [symbol, decimals] = await Promise.all([
|
|
readContract(config as any, {
|
|
abi: erc20Abi,
|
|
address: harbAddress,
|
|
functionName: "symbol",
|
|
}) as Promise<string>,
|
|
readContract(config as any, {
|
|
abi: erc20Abi,
|
|
address: harbAddress,
|
|
functionName: "decimals",
|
|
}) as Promise<number>,
|
|
]);
|
|
await provider.request({
|
|
method: "wallet_watchAsset",
|
|
params: {
|
|
type: "ERC20",
|
|
options: {
|
|
address: harbAddress,
|
|
symbol: symbol?.slice(0, 11) || "HARB",
|
|
decimals: Number.isFinite(decimals) ? decimals : 18,
|
|
},
|
|
},
|
|
});
|
|
toast.success(`Token request sent (${symbol || "HARB"})`);
|
|
} catch (error: any) {
|
|
toast.error(error?.message ?? "Failed to add token");
|
|
} finally {
|
|
addingToken.value = false;
|
|
}
|
|
}
|
|
|
|
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");
|
|
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");
|
|
}
|
|
await writeContract(config as any, {
|
|
abi: WETH_ABI,
|
|
address: weth,
|
|
functionName: "deposit",
|
|
value: amount,
|
|
});
|
|
await writeContract(config as any, {
|
|
abi: erc20Abi,
|
|
address: weth,
|
|
functionName: "approve",
|
|
args: [router, amount],
|
|
});
|
|
await writeContract(config as any, {
|
|
abi: SWAP_ROUTER_ABI,
|
|
address: router,
|
|
functionName: "exactInputSingle",
|
|
args: [
|
|
{
|
|
tokenIn: weth,
|
|
tokenOut: harb,
|
|
fee: 10_000,
|
|
recipient: caller,
|
|
amountIn: amount,
|
|
amountOutMinimum: 0n,
|
|
sqrtPriceLimitX96: 0n,
|
|
},
|
|
],
|
|
});
|
|
toast.success("Swap submitted. Watch your wallet for KRK.");
|
|
} catch (error: any) {
|
|
toast.error(error?.shortMessage ?? error?.message ?? "Swap failed");
|
|
} finally {
|
|
swapping.value = false;
|
|
}
|
|
}
|
|
|
|
async function forceRecenter() {
|
|
if (forcing.value || !txnBotRecenterUrl.value) return;
|
|
try {
|
|
forcing.value = true;
|
|
const response = await fetch(txnBotRecenterUrl.value, { method: "POST" });
|
|
let payload: any = null;
|
|
try {
|
|
payload = await response.json();
|
|
} catch {
|
|
payload = null;
|
|
}
|
|
if (!response.ok) {
|
|
const message = payload?.error ?? response.statusText ?? "recenter failed";
|
|
throw new Error(message);
|
|
}
|
|
const message = payload?.message ?? payload?.status ?? "TxnBot is recentering";
|
|
toast.success(message);
|
|
} catch (error: any) {
|
|
toast.error(error?.message ?? "recenter failed");
|
|
} finally {
|
|
forcing.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="sass">
|
|
.cheats-view
|
|
display: flex
|
|
flex-direction: column
|
|
gap: 24px
|
|
padding: 24px
|
|
@media (min-width: 992px)
|
|
padding: 48px
|
|
|
|
.cheats-header
|
|
display: flex
|
|
flex-direction: column
|
|
gap: 8px
|
|
|
|
.cheats-caption
|
|
color: #A3A3A3
|
|
max-width: 640px
|
|
|
|
.cheats-grid
|
|
display: grid
|
|
gap: 24px
|
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr))
|
|
|
|
.cheats-form
|
|
display: flex
|
|
flex-direction: column
|
|
gap: 16px
|
|
|
|
.cheats-actions
|
|
display: flex
|
|
flex-wrap: wrap
|
|
gap: 12px
|
|
|
|
.cheats-hint
|
|
font-size: 12px
|
|
color: #A3A3A3
|
|
|
|
.cheats-warning
|
|
color: #FFB347
|
|
font-size: 14px
|
|
|
|
code
|
|
font-family: "JetBrains Mono", monospace
|
|
|
|
</style>
|