basic cheat view
This commit is contained in:
parent
2be1e9d520
commit
3f137a2757
5 changed files with 503 additions and 12 deletions
|
|
@ -1,4 +1,5 @@
|
|||
const LOCAL_PONDER_URL = "http://127.0.0.1:42069/graphql";
|
||||
const LOCAL_TXNBOT_URL = "http://127.0.0.1:43069";
|
||||
|
||||
export const DEFAULT_CHAIN_ID = Number(import.meta.env.VITE_DEFAULT_CHAIN_ID ?? 84532);
|
||||
|
||||
|
|
@ -10,7 +11,8 @@ export const chainsData = [
|
|||
path: "sepolia",
|
||||
stake: "0xCd21a41a137BCAf8743E47D048F57D92398f7Da9",
|
||||
harb: "0x087F256D11fe533b0c7d372e44Ee0F9e47C89dF9",
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE",
|
||||
cheats: null
|
||||
}, {
|
||||
// base-sepolia (local dev default)
|
||||
id: 84532,
|
||||
|
|
@ -18,7 +20,12 @@ export const chainsData = [
|
|||
path: "sepoliabase",
|
||||
stake: "0xe28020BCdEeAf2779dd47c670A8eFC2973316EE2",
|
||||
harb: "0x22c264Ecf8D4E49D1E3CabD8DD39b7C4Ab51C1B8",
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE",
|
||||
cheats: {
|
||||
weth: "0x4200000000000000000000000000000000000006",
|
||||
swapRouter: "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4",
|
||||
txnBot: import.meta.env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? LOCAL_TXNBOT_URL
|
||||
}
|
||||
},
|
||||
{
|
||||
// base mainnet
|
||||
|
|
@ -27,7 +34,12 @@ export const chainsData = [
|
|||
path: "base",
|
||||
stake: "0xed70707fab05d973ad41eae8d17e2bcd36192cfc",
|
||||
harb: "0x45caa5929f6ee038039984205bdecf968b954820",
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE"
|
||||
uniswap: "https://app.uniswap.org/swap?chain=mainnet&inputCurrency=NATIVE",
|
||||
cheats: {
|
||||
weth: "0x4200000000000000000000000000000000000006",
|
||||
swapRouter: "",
|
||||
txnBot: import.meta.env.VITE_TXNBOT_BASE ?? ""
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,15 @@ const router = createRouter({
|
|||
, },
|
||||
component: () => import("../views/LoginView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/cheats",
|
||||
name: "cheats",
|
||||
meta: {
|
||||
title: "Cheats",
|
||||
layout: 'NavbarLayout'
|
||||
},
|
||||
component: () => import("../views/CheatsView.vue"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
|||
429
web-app/src/views/CheatsView.vue
Normal file
429
web-app/src/views/CheatsView.vue
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue