better backend comms

This commit is contained in:
johba 2025-09-24 09:41:28 +02:00
parent d0e8623cf9
commit 02a057622c
17 changed files with 1054 additions and 134 deletions

1
.gitignore vendored
View file

@ -25,3 +25,4 @@ onchain/node_modules/
ponder-repo
tmp
foundry.lock
services/ponder/.env.local

View file

@ -13,6 +13,8 @@
- `BASE_SEPOLIA`: Public Base Sepolia testnet
- `BASE`: Base mainnet
> **Local dev**: Always start the stack via `nohup ./scripts/local_env.sh start &` (never run services individually); stop with `./scripts/local_env.sh stop`.
## Execution Workflow
1. **Contracts**
- Build/test via `forge build` and `forge test` inside `onchain/`.
@ -67,7 +69,7 @@
- `PONDER_NETWORK=BASE_SEPOLIA_LOCAL_FORK npm run dev`
- `curl -X POST http://localhost:42069/graphql -d '{"query":"{ stats(id:\"0x01\"){kraikenTotalSupply}}"}'`
- `curl http://127.0.0.1:43069/status`
- `./scripts/local_env.sh start` boots Anvil+contracts+ponder+frontend+txnBot; stop with Ctrl+C or `./scripts/local_env.sh stop`.
- `nohup ./scripts/local_env.sh start &` boots Anvil+contracts+ponder+frontend+txnBot; stop with Ctrl+C or `./scripts/local_env.sh stop` when you want to tear it down.
## Refactor Backlog
- Replace the temporary `any` shims in `services/ponder/src/kraiken.ts` and `services/ponder/src/stake.ts` by importing the official 0.13 handler types instead of stubbing `ponder-env.d.ts`.

View file

@ -24,6 +24,7 @@ ANVIL_RPC="http://127.0.0.1:8545"
GRAPHQL_HEALTH="http://127.0.0.1:42069/health"
GRAPHQL_ENDPOINT="http://127.0.0.1:42069/graphql"
FRONTEND_URL="http://127.0.0.1:5173"
LOCAL_TXNBOT_URL="http://127.0.0.1:43069"
FOUNDRY_BIN=${FOUNDRY_BIN:-"$HOME/.foundry/bin"}
FORGE="$FOUNDRY_BIN/forge"
@ -172,10 +173,17 @@ ensure_dependencies() {
start_anvil() {
if [[ -f "$ANVIL_PID_FILE" ]]; then
log "Anvil already running (pid $(cat "$ANVIL_PID_FILE"))"
local existing_pid
existing_pid="$(cat "$ANVIL_PID_FILE")"
if kill -0 "$existing_pid" 2>/dev/null; then
log "Anvil already running (pid $existing_pid)"
return
fi
log "Found stale Anvil pid file (pid $existing_pid); restarting Anvil"
rm -f "$ANVIL_PID_FILE"
fi
log "Starting Anvil (forking $FORK_URL)"
local anvil_args=("--fork-url" "$FORK_URL" "--chain-id" 31337 "--block-time" 1 \
"--host" 127.0.0.1 "--port" 8545)
@ -266,6 +274,16 @@ bootstrap_liquidity_manager() {
log "Calling recenter()"
"$CAST" send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \
"$LIQUIDITY_MANAGER" "recenter()" >>"$SETUP_LOG" 2>&1
if [[ -n "$TXNBOT_ADDRESS" ]]; then
log "Transferring recenter access to txnBot $TXNBOT_ADDRESS"
"$CAST" rpc --rpc-url "$ANVIL_RPC" anvil_impersonateAccount "$FEE_DEST" >/dev/null
"$CAST" send --rpc-url "$ANVIL_RPC" --from "$FEE_DEST" --unlocked \
"$LIQUIDITY_MANAGER" "setRecenterAccess(address)" "$TXNBOT_ADDRESS" >>"$SETUP_LOG" 2>&1
"$CAST" rpc --rpc-url "$ANVIL_RPC" anvil_stopImpersonatingAccount "$FEE_DEST" >/dev/null
else
log "TXNBOT_ADDRESS not set; recenter access left with deployer"
fi
}
prepare_application_state() {
@ -320,6 +338,14 @@ fund_txnbot_wallet() {
"$CAST" send --rpc-url "$ANVIL_RPC" --private-key "$DEPLOYER_PK" \
"$TXNBOT_ADDRESS" --value "$TXNBOT_FUND_VALUE" >>"$SETUP_LOG" 2>&1 || \
log "Funding txnBot wallet failed (see setup log)"
# Ensure local fork balance reflects the funding even if the upstream state is zero
local fund_value_wei
fund_value_wei="$("$CAST" --to-unit "$TXNBOT_FUND_VALUE" wei)"
local fund_value_hex
fund_value_hex="$("$CAST" --to-hex "$fund_value_wei")"
"$CAST" rpc --rpc-url "$ANVIL_RPC" anvil_setBalance "$TXNBOT_ADDRESS" "$fund_value_hex" \
>>"$SETUP_LOG" 2>&1
}
start_txnbot() {
@ -355,13 +381,26 @@ start_ponder() {
start_frontend() {
if [[ -f "$WEBAPP_PID_FILE" ]]; then
log "Frontend already running (pid $(cat "$WEBAPP_PID_FILE"))"
log "Frontend already running (pid $(cat \"$WEBAPP_PID_FILE\"))"
return
fi
source_addresses || { log "Contract addresses not found"; exit 1; }
local vite_env=(
"VITE_DEFAULT_CHAIN_ID=31337"
"VITE_LOCAL_RPC_URL=/rpc/anvil"
"VITE_LOCAL_RPC_PROXY_TARGET=$ANVIL_RPC"
"VITE_KRAIKEN_ADDRESS=$KRAIKEN"
"VITE_STAKE_ADDRESS=$STAKE"
"VITE_SWAP_ROUTER=$SWAP_ROUTER"
"VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK=$GRAPHQL_ENDPOINT"
"VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK=$LOCAL_TXNBOT_URL"
)
log "Starting frontend (Vite dev server)"
pushd "$ROOT_DIR/web-app" >/dev/null
npm run dev -- --host 0.0.0.0 --port 5173 >"$WEBAPP_LOG" 2>&1 &
env "${vite_env[@]}" npm run dev -- --host 0.0.0.0 --port 5173 >"$WEBAPP_LOG" 2>&1 &
popd >/dev/null
echo $! >"$WEBAPP_PID_FILE"

View file

@ -1,8 +0,0 @@
# Auto-generated by local_env.sh
PONDER_NETWORK=BASE_SEPOLIA_LOCAL_FORK
KRAIKEN_ADDRESS=0x56186c1e64ca8043def78d06aff222212ea5df71
STAKE_ADDRESS=0x056e4a859558a3975761abd7385506bc4d8a8e60
START_BLOCK=31443298
# Use PostgreSQL connection
DATABASE_URL=postgresql://ponder:ponder_local@localhost/ponder_local
DATABASE_SCHEMA=ponder_local_31443298

View file

@ -1,10 +1,19 @@
import { Hono } from "hono";
import { cors } from "hono/cors";
import { client, graphql } from "ponder";
import { db } from "ponder:api";
import schema from "ponder:schema";
import { Hono } from "hono";
import { client, graphql } from "ponder";
const app = new Hono();
const allowedOrigins = process.env.PONDER_CORS_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
app.use("/*", cors({
origin: allowedOrigins?.length ? allowedOrigins : "*",
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Apollo-Require-Preflight"],
}));
// SQL endpoint
app.use("/sql/*", client({ db, schema }));

View file

@ -99,22 +99,46 @@ export async function ensureStatsExists(context: any, timestamp?: bigint) {
let statsData = await context.db.find(stats, { id: STATS_ID });
if (!statsData) {
const { client, contracts } = context;
const readWithFallback = async <T>(fn: () => Promise<T>, fallback: T, label: string): Promise<T> => {
try {
return await fn();
} catch (error) {
console.warn(`[stats.ensureStatsExists] Falling back for ${label}`, error);
return fallback;
}
};
const [kraikenTotalSupply, stakeTotalSupply, outstandingStake] = await Promise.all([
readWithFallback(
() =>
client.readContract({
abi: contracts.Kraiken.abi,
address: contracts.Kraiken.address,
functionName: "totalSupply",
}),
0n,
"Kraiken.totalSupply",
),
readWithFallback(
() =>
client.readContract({
abi: contracts.Stake.abi,
address: contracts.Stake.address,
functionName: "totalSupply",
}),
0n,
"Stake.totalSupply",
),
readWithFallback(
() =>
client.readContract({
abi: contracts.Stake.abi,
address: contracts.Stake.address,
functionName: "outstandingStake",
}),
0n,
"Stake.outstandingStake",
),
]);
cachedStakeTotalSupply = stakeTotalSupply;

View file

@ -140,7 +140,7 @@ import { formatBigIntDivision, InsertCommaNumber, formatBigNumber, bigInt2Number
// import { bytesToUint256, uint256ToBytes } from "harb-lib";
// import { getSnatchList } from "harb-lib/dist/";
import { formatUnits } from "viem";
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
import { loadPositions, usePositions, type Position } from "@/composables/usePositions";
import {
calculateSnatchShortfall,
selectSnatchPositions,
@ -238,7 +238,7 @@ async function stakeSnatch() {
}
stakeSnatchLoading.value = true;
await new Promise((resolve) => setTimeout(resolve, 10000));
await loadActivePositions();
await loadPositions();
await loadStats();
stakeSnatchLoading.value = false;
}

View file

@ -12,7 +12,7 @@ import {bigInt2Number, formatBigIntDivision} from "@/utils/helper";
import { computed, ref } from "vue";
import { useStatCollection } from "@/composables/useStatCollection";
import { useStake } from "@/composables/useStake";
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
import { usePositions, type Position } from "@/composables/usePositions";
import { useDark } from "@/composables/useDark";
const { darkTheme } = useDark();

View file

@ -15,6 +15,12 @@ import logger from "@/utils/logger";
const rawActivePositions = ref<Array<Position>>([]);
const rawClosedPositoins = ref<Array<Position>>([]);
const loading = ref(false);
const positionsError = ref<string | null>(null);
const POSITIONS_RETRY_BASE_DELAY = 1_500;
const POSITIONS_RETRY_MAX_DELAY = 60_000;
const positionsRetryDelayMs = ref(POSITIONS_RETRY_BASE_DELAY);
const GRAPHQL_TIMEOUT_MS = 15_000;
let positionsRetryTimer: number | null = null;
const chain = useChain();
const activePositions = computed(() => {
const account = getAccount(config as any);
@ -106,14 +112,16 @@ const tresholdValue = computed(() => {
return avg / 2;
});
export async function loadActivePositions() {
export async function loadActivePositions(endpoint?: string) {
logger.info(`loadActivePositions for chain: ${chainData.value?.path}`);
if (!chainData.value?.graphql) {
return [];
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim();
if (!targetEndpoint) {
throw new Error("GraphQL endpoint not configured for this chain.");
}
console.log("chainData.value?.graphql", chainData.value?.graphql);
const res = await axios.post(chainData.value?.graphql, {
console.log("chainData.value?.graphql", targetEndpoint);
const res = await axios.post(targetEndpoint, {
query: `query ActivePositions {
positionss(where: { status: "Active" }, orderBy: "taxRate", orderDirection: "asc", limit: 1000) {
items {
@ -132,7 +140,12 @@ export async function loadActivePositions() {
}
}
}`,
});
}, { timeout: GRAPHQL_TIMEOUT_MS });
const errors = res.data?.errors;
if (Array.isArray(errors) && errors.length > 0) {
throw new Error(errors.map((err: any) => err?.message ?? "GraphQL error").join(", "));
}
const items = res.data?.data?.positionss?.items ?? [];
return items.map((item: any) => ({
@ -147,12 +160,13 @@ function formatId(id: Hex) {
return bigIntId;
}
export async function loadMyClosedPositions(account: GetAccountReturnType) {
export async function loadMyClosedPositions(endpoint: string | undefined, account: GetAccountReturnType) {
logger.info(`loadMyClosedPositions for chain: ${chainData.value?.path}`);
if (!chainData.value?.graphql) {
return [];
const targetEndpoint = endpoint ?? chainData.value?.graphql?.trim();
if (!targetEndpoint) {
throw new Error("GraphQL endpoint not configured for this chain.");
}
const res = await axios.post(chainData.value?.graphql, {
const res = await axios.post(targetEndpoint, {
query: `query ClosedPositions {
positionss(where: { status: "Closed", owner: "${account.address?.toLowerCase()}" }, limit: 1000) {
items {
@ -171,10 +185,10 @@ export async function loadMyClosedPositions(account: GetAccountReturnType) {
}
}
}`,
});
if (res.data.errors?.length > 0) {
console.error("todo nur laden, wenn eingeloggt");
return [];
}, { timeout: GRAPHQL_TIMEOUT_MS });
const errors = res.data?.errors;
if (Array.isArray(errors) && errors.length > 0) {
throw new Error(errors.map((err: any) => err?.message ?? "GraphQL error").join(", "));
}
const items = res.data?.data?.positionss?.items ?? [];
return items.map((item: any) => ({
@ -186,20 +200,84 @@ export async function loadMyClosedPositions(account: GetAccountReturnType) {
export async function loadPositions() {
loading.value = true;
rawActivePositions.value = await loadActivePositions();
//optimal wäre es: laden, wenn new Position meine Position geclosed hat
const account = getAccount(config as any);
if (account.address) {
rawClosedPositoins.value = await loadMyClosedPositions(account);
const endpoint = chainData.value?.graphql?.trim();
if (!endpoint) {
rawActivePositions.value = [];
rawClosedPositoins.value = [];
positionsError.value = "GraphQL endpoint not configured for this chain.";
clearPositionsRetryTimer();
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY;
loading.value = false;
return;
}
try {
rawActivePositions.value = await loadActivePositions(endpoint);
const account = getAccount(config as any);
if (account.address) {
rawClosedPositoins.value = await loadMyClosedPositions(endpoint, account);
} else {
rawClosedPositoins.value = [];
}
positionsError.value = null;
positionsRetryDelayMs.value = POSITIONS_RETRY_BASE_DELAY;
clearPositionsRetryTimer();
} catch (error) {
console.warn("[positions] loadPositions() failed", error);
rawActivePositions.value = [];
rawClosedPositoins.value = [];
positionsError.value = formatGraphqlError(error);
schedulePositionsRetry();
} finally {
loading.value = false;
}
}
let unwatch: WatchEventReturnType;
let unwatchPositionRemovedEvent: WatchEventReturnType;
let unwatchChainSwitch: WatchChainIdReturnType;
let unwatchAccountChanged: WatchAccountReturnType;
let unwatch: WatchEventReturnType | null;
let unwatchPositionRemovedEvent: WatchEventReturnType | null;
let unwatchChainSwitch: WatchChainIdReturnType | null;
let unwatchAccountChanged: WatchAccountReturnType | null;
function formatGraphqlError(error: unknown): string {
if (axios.isAxiosError(error)) {
const responseErrors = (error.response?.data as any)?.errors;
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
return responseErrors.map((err: any) => err?.message ?? "GraphQL error").join(", ");
}
if (error.response?.status) {
return `GraphQL request failed with status ${error.response.status}`;
}
if (error.message) {
return error.message;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return "Unknown GraphQL error";
}
function clearPositionsRetryTimer() {
if (positionsRetryTimer !== null) {
clearTimeout(positionsRetryTimer);
positionsRetryTimer = null;
}
}
function schedulePositionsRetry() {
if (typeof window === "undefined") {
return;
}
if (positionsRetryTimer !== null) {
return;
}
const delay = positionsRetryDelayMs.value;
positionsRetryTimer = window.setTimeout(async () => {
positionsRetryTimer = null;
await loadPositions();
}, delay);
positionsRetryDelayMs.value = Math.min(positionsRetryDelayMs.value * 2, POSITIONS_RETRY_MAX_DELAY);
}
export function usePositions() {
function watchEvent() {
unwatch = watchContractEvent(config as any, {
@ -231,7 +309,7 @@ export function usePositions() {
//initial loading positions
if (activePositions.value.length < 1 && loading.value === false) {
loadPositions();
await loadPositions();
// await getMinStake();
}
@ -260,12 +338,23 @@ export function usePositions() {
});
onUnmounted(() => {
// if (unwatch) {
// unwatch();
// }
// if (unwatchPositionRemovedEvent) {
// unwatchPositionRemovedEvent();
// }
if (unwatch) {
unwatch();
unwatch = null;
}
if (unwatchPositionRemovedEvent) {
unwatchPositionRemovedEvent();
unwatchPositionRemovedEvent = null;
}
if (unwatchChainSwitch) {
unwatchChainSwitch();
unwatchChainSwitch = null;
}
if (unwatchAccountChanged) {
unwatchAccountChanged();
unwatchAccountChanged = null;
}
clearPositionsRetryTimer();
});
function createRandomPosition(amount: number = 1) {
@ -317,5 +406,7 @@ export function usePositions() {
watchEvent,
watchPositionRemoved,
createRandomPosition,
positionsError,
loading,
};
}

View file

@ -8,6 +8,10 @@ import type { WatchBlocksReturnType } from "viem";
import { bigInt2Number } from "@/utils/helper";
const demo = sessionStorage.getItem("demo") === "true";
const GRAPHQL_TIMEOUT_MS = 15_000;
const RETRY_BASE_DELAY_MS = 1_500;
const RETRY_MAX_DELAY_MS = 60_000;
interface StatsRecord {
burnNextHourProjected: string;
burnedLastDay: string;
@ -27,14 +31,54 @@ interface StatsRecord {
const rawStatsCollections = ref<Array<StatsRecord>>([]);
const loading = ref(false);
const initialized = ref(false);
const statsError = ref<string | null>(null);
const statsRetryDelayMs = ref(RETRY_BASE_DELAY_MS);
let statsRetryTimer: number | null = null;
export async function loadStatsCollection() {
logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`);
if (!chainData.value?.graphql) {
return [];
function formatGraphqlError(error: unknown): string {
if (axios.isAxiosError(error)) {
const responseErrors = (error.response?.data as any)?.errors;
if (Array.isArray(responseErrors) && responseErrors.length > 0) {
return responseErrors.map((err: any) => err?.message ?? "GraphQL error").join(", ");
}
if (error.response?.status) {
return `GraphQL request failed with status ${error.response.status}`;
}
if (error.message) {
return error.message;
}
}
if (error instanceof Error && error.message) {
return error.message;
}
return "Unknown GraphQL error";
}
const res = await axios.post(chainData.value?.graphql, {
function clearStatsRetryTimer() {
if (statsRetryTimer !== null) {
clearTimeout(statsRetryTimer);
statsRetryTimer = null;
}
}
function scheduleStatsRetry() {
if (typeof window === "undefined") {
return;
}
if (statsRetryTimer !== null) {
return;
}
const delay = statsRetryDelayMs.value;
statsRetryTimer = window.setTimeout(async () => {
statsRetryTimer = null;
await loadStats();
}, delay);
statsRetryDelayMs.value = Math.min(statsRetryDelayMs.value * 2, RETRY_MAX_DELAY_MS);
}
export async function loadStatsCollection(endpoint: string) {
logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`);
const res = await axios.post(endpoint, {
query: `query StatsQuery {
stats(id: "0x01") {
burnNextHourProjected
@ -52,11 +96,16 @@ export async function loadStatsCollection() {
totalMinted
}
}`,
});
}, { timeout: GRAPHQL_TIMEOUT_MS });
const errors = res.data?.errors;
if (Array.isArray(errors) && errors.length > 0) {
throw new Error(errors.map((err: any) => err?.message ?? "GraphQL error").join(", "));
}
const stats = res.data?.data?.stats as StatsRecord | undefined;
if (!stats) {
return [];
throw new Error("Stats entity not found in GraphQL response");
}
return [{ ...stats, kraikenTotalSupply: stats.kraikenTotalSupply }];
@ -178,10 +227,31 @@ const claimedSlots = computed(() => {
export async function loadStats() {
loading.value = true;
rawStatsCollections.value = await loadStatsCollection();
const endpoint = chainData.value?.graphql?.trim();
if (!endpoint) {
rawStatsCollections.value = [];
statsError.value = "GraphQL endpoint not configured for this chain.";
clearStatsRetryTimer();
statsRetryDelayMs.value = RETRY_BASE_DELAY_MS;
loading.value = false;
initialized.value = true;
return;
}
try {
rawStatsCollections.value = await loadStatsCollection(endpoint);
statsError.value = null;
statsRetryDelayMs.value = RETRY_BASE_DELAY_MS;
clearStatsRetryTimer();
} catch (error) {
console.warn("[stats] loadStats() failed", error);
rawStatsCollections.value = [];
statsError.value = formatGraphqlError(error);
scheduleStatsRetry();
} finally {
loading.value = false;
initialized.value = true;
}
}
let unwatch: any = null;
@ -212,6 +282,7 @@ export function useStatCollection() {
// })
}
onUnmounted(() => {
clearStatsRetryTimer();
unwatch();
});
@ -227,5 +298,7 @@ export function useStatCollection() {
maxSlots,
stakeableSupply,
claimedSlots,
statsError,
loading,
});
}

View file

@ -1,6 +1,6 @@
import { ref, onMounted, onUnmounted, reactive, computed } from "vue";
import { type Ref } from "vue";
import { getBalance, watchAccount, watchChainId } from "@wagmi/core";
import { getAccount, getBalance, watchAccount, watchChainId } from "@wagmi/core";
import { type WatchAccountReturnType, type GetAccountReturnType, type GetBalanceReturnType } from "@wagmi/core";
import { config } from "@/wagmi";
import { getAllowance, HarbContract, getNonce } from "@/contracts/harb";
@ -15,18 +15,10 @@ const balance = ref<GetBalanceReturnType>({
symbol: "",
formatted: ""
});
const account = ref<GetAccountReturnType>({
address: undefined,
addresses: undefined,
chain: undefined,
chainId: undefined,
connector: undefined,
isConnected: false,
isConnecting: false,
isDisconnected: true,
isReconnecting: false,
status: "disconnected",
});
const account = ref<GetAccountReturnType>(getAccount(config as any));
if (!account.value.chainId) {
(account.value as any).chainId = DEFAULT_CHAIN_ID;
}
const selectedChainId = computed(() => account.value.chainId ?? DEFAULT_CHAIN_ID);

View file

@ -1,9 +1,55 @@
const LOCAL_PONDER_URL = "http://127.0.0.1:42069/graphql";
const LOCAL_TXNBOT_URL = "http://127.0.0.1:43069";
import deploymentsLocal from "../../onchain/deployments-local.json";
export const DEFAULT_CHAIN_ID = Number(import.meta.env.VITE_DEFAULT_CHAIN_ID ?? 84532);
const LOCAL_PONDER_URL = import.meta.env.VITE_PONDER_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:42069/graphql";
const LOCAL_TXNBOT_URL = import.meta.env.VITE_TXNBOT_BASE_SEPOLIA_LOCAL_FORK ?? "http://127.0.0.1:43069";
const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? "/rpc/anvil";
const localContracts = (deploymentsLocal as any)?.contracts ?? {};
const localInfra = (deploymentsLocal as any)?.infrastructure ?? {};
const LOCAL_KRAIKEN = (localContracts.Kraiken ?? "").trim();
const LOCAL_STAKE = (localContracts.Stake ?? "").trim();
const LOCAL_LM = (localContracts.LiquidityManager ?? "").trim();
const LOCAL_WETH = (localInfra.weth ?? "0x4200000000000000000000000000000000000006").trim();
const LOCAL_ROUTER = "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4";
function detectDefaultChainId(): number {
const envValue = import.meta.env.VITE_DEFAULT_CHAIN_ID;
if (envValue) {
const parsed = Number(envValue);
if (Number.isFinite(parsed)) {
return parsed;
}
}
if (typeof window !== "undefined") {
const host = window.location.hostname.toLowerCase();
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
return 31337;
}
}
return 84532;
}
export const DEFAULT_CHAIN_ID = detectDefaultChainId();
export const chainsData = [
{
// local base sepolia fork
id: 31337,
graphql: LOCAL_PONDER_URL,
path: "local",
stake: LOCAL_STAKE,
harb: LOCAL_KRAIKEN,
uniswap: "",
cheats: {
weth: LOCAL_WETH,
swapRouter: LOCAL_ROUTER,
liquidityManager: LOCAL_LM,
txnBot: LOCAL_TXNBOT_URL,
rpc: LOCAL_RPC_URL,
}
},
{
// sepolia
id: 11155111,

View file

@ -21,10 +21,66 @@
@click="addToken"
variant="secondary"
>{{ addingToken ? "Adding…" : "Add KRK token" }}</f-button>
<f-button
:disabled="!canAddWeth || addingWeth"
@click="addWethToken"
variant="secondary"
>{{ addingWeth ? "Adding…" : "Add WETH token" }}</f-button>
</div>
</div>
</f-card>
<f-card title="Liquidity Snapshot">
<div class="cheats-form">
<template v-if="!cheatConfig">
<p class="cheats-warning">Cheat helpers are disabled for this network.</p>
</template>
<template v-else>
<template v-if="statsError">
<p class="cheats-warning">{{ statsError }}</p>
</template>
<template v-else-if="statsLoading">
<p class="cheats-hint">Loading liquidity metrics</p>
</template>
<template v-else-if="liquidityStats">
<div class="liquidity-meta">
<span>Current tick: {{ liquidityStats.currentTick }}</span>
<span>Token0 is WETH: {{ liquidityStats.token0isWeth ? "Yes" : "No" }}</span>
</div>
<div class="liquidity-meta">
<span>Buy depth to discovery edge: {{ liquidityStats.buyDepthFormatted }} ETH</span>
</div>
<table class="liquidity-table">
<thead>
<tr>
<th>Stage</th>
<th>Tick Lower</th>
<th>Tick Upper</th>
<th>Liquidity</th>
</tr>
</thead>
<tbody>
<tr v-for="position in liquidityStats.positions" :key="position.label">
<td>{{ position.label }}</td>
<td>{{ position.tickLower }}</td>
<td>{{ position.tickUpper }}</td>
<td>{{ position.liquidityFormatted }}</td>
</tr>
</tbody>
</table>
</template>
<template v-else>
<p class="cheats-hint">Liquidity stats unavailable for this network.</p>
</template>
</template>
<f-button
variant="secondary"
:disabled="statsLoading"
@click="refreshLiquidityStats"
>{{ statsLoading ? "Refreshing…" : "Refresh stats" }}</f-button>
</div>
</f-card>
<div class="cheats-grid">
<f-card title="Mint ETH">
<div class="cheats-form">
@ -65,7 +121,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { computed, onMounted, 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";
@ -74,19 +130,27 @@ import { useToast } from "vue-toastification";
import { useAccount } from "@wagmi/vue";
import { getChain, DEFAULT_CHAIN_ID } from "@/config";
import { readContract, writeContract } from "@wagmi/core";
import HarbJson from "@/assets/contracts/harb.json";
import {
createPublicClient,
erc20Abi,
formatEther,
getAddress,
http,
isAddress,
maxUint256,
parseEther,
toHex,
type Address
zeroAddress,
type Address,
type Abi
} from "viem";
const toast = useToast();
const { address, chainId } = useAccount();
const rpcUrl = ref("http://127.0.0.1:8545");
const rpcUrl = ref("");
let lastAutoRpcUrl = "";
const mintTarget = ref("");
const mintAmount = ref("10");
@ -99,12 +163,32 @@ const forcing = ref(false);
const addingNetwork = ref(false);
const addingToken = ref(false);
const addingWeth = ref(false);
watch(address, (value) => {
if (value) {
mintTarget.value = value;
interface PositionState {
liquidity: bigint;
tickLower: number;
tickUpper: number;
}
}, { immediate: true });
interface PositionView extends PositionState {
label: (typeof POSITION_STAGE_LABELS)[number];
liquidityFormatted: string;
}
interface LiquidityStats {
positions: PositionView[];
currentTick: number;
poolAddress: Address;
token0isWeth: boolean;
buyDepthWei: bigint;
buyDepthFormatted: string;
}
const liquidityStats = ref<LiquidityStats | null>(null);
const statsLoading = ref(false);
const statsError = ref<string | null>(null);
let statsRequestId = 0;
const resolvedChainId = computed(() => chainId.value ?? DEFAULT_CHAIN_ID);
const chainConfig = computed(() => getChain(resolvedChainId.value));
@ -113,6 +197,7 @@ 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 canAddWeth = computed(() => Boolean(hasWalletProvider.value && cheatConfig.value?.weth));
const txnBotRecenterUrl = computed(() => {
const baseUrl = cheatConfig.value?.txnBot?.trim();
if (!baseUrl) return null;
@ -120,6 +205,38 @@ const txnBotRecenterUrl = computed(() => {
});
const canTriggerRecenter = computed(() => Boolean(txnBotRecenterUrl.value));
watch(address, (value) => {
if (value) {
mintTarget.value = value;
}
}, { immediate: true });
watch(
() => (cheatConfig.value?.rpc ?? "").trim(),
(value) => {
const trimmed = value;
if (!rpcUrl.value || rpcUrl.value === lastAutoRpcUrl) {
rpcUrl.value = trimmed;
}
lastAutoRpcUrl = trimmed;
},
{ immediate: true }
);
watch(
() => [rpcUrl.value, resolvedChainId.value, chainConfig.value?.harb, cheatConfig.value?.weth, cheatConfig.value?.swapRouter],
() => {
void loadLiquidityStats();
},
{ immediate: true }
);
onMounted(() => {
if (!liquidityStats.value && !statsLoading.value) {
void loadLiquidityStats();
}
});
function resolveChainName(chainId: number): string {
switch (chainId) {
case 31337:
@ -135,6 +252,17 @@ function resolveChainName(chainId: number): string {
}
}
function resolveRpcUrl(): string {
const value = rpcUrl.value.trim();
if (!value) {
return value;
}
if (value.startsWith("/") && typeof window !== "undefined" && window.location?.origin) {
return `${window.location.origin}${value}`;
}
return value;
}
const WETH_ABI = [
{
inputs: [],
@ -146,6 +274,13 @@ const WETH_ABI = [
];
const SWAP_ROUTER_ABI = [
{
inputs: [],
name: "factory",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
@ -170,11 +305,361 @@ const SWAP_ROUTER_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",
},
];
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",
},
];
const LIQUIDITY_MANAGER_POSITIONS_ABI = [
{
inputs: [{ internalType: "uint8", name: "", type: "uint8" }],
name: "positions",
outputs: [
{ internalType: "uint128", name: "liquidity", type: "uint128" },
{ internalType: "int24", name: "tickLower", type: "int24" },
{ internalType: "int24", name: "tickUpper", type: "int24" },
],
stateMutability: "view",
type: "function",
},
];
const POSITION_STAGE_LABELS = ["Floor", "Anchor", "Discovery"] as const;
const POOL_FEE = 10_000;
const MIN_TICK = -887272;
const MAX_TICK = 887272;
const Q96 = 2n ** 96n;
const MAX_UINT256_BIGINT = (1n << 256n) - 1n;
function formatBigInt(value: bigint): string {
const sign = value < 0n ? "-" : "";
const digits = (value < 0n ? -value : value).toString();
return sign + digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function formatEth(value: bigint): string {
if (value === 0n) return "0";
const formatted = formatEther(value);
const [whole, frac = ""] = formatted.split(".");
const trimmedFrac = frac.replace(/0+$/, "").slice(0, 6);
return trimmedFrac ? `${whole}.${trimmedFrac}` : whole;
}
function getSqrtRatioAtTick(tick: number): bigint {
if (tick < MIN_TICK || tick > MAX_TICK) {
throw new Error("Tick out of range");
}
const absTick = BigInt(tick < 0 ? -tick : tick);
let ratio = (absTick & 0x1n) !== 0n
? 0xfffcb933bd6fad37aa2d162d1a594001n
: 0x100000000000000000000000000000000n;
if ((absTick & 0x2n) !== 0n) ratio = (ratio * 0xfff97272373d413259a46990580e213an) >> 128n;
if ((absTick & 0x4n) !== 0n) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdccn) >> 128n;
if ((absTick & 0x8n) !== 0n) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0n) >> 128n;
if ((absTick & 0x10n) !== 0n) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644n) >> 128n;
if ((absTick & 0x20n) !== 0n) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0n) >> 128n;
if ((absTick & 0x40n) !== 0n) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861n) >> 128n;
if ((absTick & 0x80n) !== 0n) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053n) >> 128n;
if ((absTick & 0x100n) !== 0n) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4n) >> 128n;
if ((absTick & 0x200n) !== 0n) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54n) >> 128n;
if ((absTick & 0x400n) !== 0n) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3n) >> 128n;
if ((absTick & 0x800n) !== 0n) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9n) >> 128n;
if ((absTick & 0x1000n) !== 0n) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825n) >> 128n;
if ((absTick & 0x2000n) !== 0n) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5n) >> 128n;
if ((absTick & 0x4000n) !== 0n) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7n) >> 128n;
if ((absTick & 0x8000n) !== 0n) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6n) >> 128n;
if ((absTick & 0x10000n) !== 0n) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9n) >> 128n;
if ((absTick & 0x20000n) !== 0n) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604n) >> 128n;
if ((absTick & 0x40000n) !== 0n) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98n) >> 128n;
if ((absTick & 0x80000n) !== 0n) ratio = (ratio * 0x48a170391f7dc42444e8fa2n) >> 128n;
if (tick > 0) {
ratio = MAX_UINT256_BIGINT / ratio;
}
const result = ratio >> 32n;
const shouldRoundUp = (ratio & 0xffffffffn) !== 0n;
return shouldRoundUp ? result + 1n : result;
}
function getAmount0ForLiquidity(sqrtRatioAX96: bigint, sqrtRatioBX96: bigint, liquidity: bigint): bigint {
if (sqrtRatioAX96 > sqrtRatioBX96) {
[sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
}
if (liquidity === 0n || sqrtRatioAX96 === 0n) {
return 0n;
}
const numerator = liquidity << 96n;
const intermediate = (sqrtRatioBX96 - sqrtRatioAX96) * numerator;
return intermediate / sqrtRatioBX96 / sqrtRatioAX96;
}
function getAmount1ForLiquidity(sqrtRatioAX96: bigint, sqrtRatioBX96: bigint, liquidity: bigint): bigint {
if (sqrtRatioAX96 > sqrtRatioBX96) {
[sqrtRatioAX96, sqrtRatioBX96] = [sqrtRatioBX96, sqrtRatioAX96];
}
if (liquidity === 0n) {
return 0n;
}
return (liquidity * (sqrtRatioBX96 - sqrtRatioAX96)) / Q96;
}
function calculateEthToMoveBetweenTicks(fromTick: number, toTick: number, liquidity: bigint): bigint {
if (fromTick >= toTick || liquidity === 0n) {
return 0n;
}
const sqrtFrom = getSqrtRatioAtTick(fromTick);
const sqrtTo = getSqrtRatioAtTick(toTick);
return getAmount0ForLiquidity(sqrtFrom, sqrtTo, liquidity);
}
function calculateEthToMoveBetweenTicksDown(fromTick: number, toTick: number, liquidity: bigint): bigint {
if (fromTick <= toTick || liquidity === 0n) {
return 0n;
}
const sqrtFrom = getSqrtRatioAtTick(fromTick);
const sqrtTo = getSqrtRatioAtTick(toTick);
return getAmount1ForLiquidity(sqrtTo, sqrtFrom, liquidity);
}
function calculateBuyDepth(
token0isWeth: boolean,
currentTick: number,
anchor: PositionState,
discovery: PositionState
): bigint {
if (anchor.liquidity === 0n && discovery.liquidity === 0n) {
return 0n;
}
if (token0isWeth) {
const targetTick = Math.max(discovery.tickUpper, anchor.tickUpper);
if (currentTick >= targetTick) {
return 0n;
}
let total = 0n;
if (currentTick >= anchor.tickLower && currentTick < anchor.tickUpper && anchor.liquidity > 0n) {
const anchorEnd = Math.min(targetTick, anchor.tickUpper);
total += calculateEthToMoveBetweenTicks(currentTick, anchorEnd, anchor.liquidity);
}
if (targetTick > anchor.tickUpper && discovery.liquidity > 0n) {
const discoveryStart = Math.max(currentTick, discovery.tickLower);
if (discoveryStart < discovery.tickUpper) {
total += calculateEthToMoveBetweenTicks(discoveryStart, targetTick, discovery.liquidity);
}
}
return total;
}
const targetTick = Math.min(discovery.tickLower, anchor.tickLower);
if (currentTick <= targetTick) {
return 0n;
}
let total = 0n;
if (currentTick <= anchor.tickUpper && currentTick > anchor.tickLower && anchor.liquidity > 0n) {
const anchorEnd = Math.max(targetTick, anchor.tickLower);
total += calculateEthToMoveBetweenTicksDown(currentTick, anchorEnd, anchor.liquidity);
}
if (targetTick < anchor.tickLower && discovery.liquidity > 0n) {
const discoveryStart = Math.min(currentTick, discovery.tickUpper);
if (discoveryStart > discovery.tickLower) {
total += calculateEthToMoveBetweenTicksDown(discoveryStart, targetTick, discovery.liquidity);
}
}
return total;
}
async function loadLiquidityStats({ notify = false }: { notify?: boolean } = {}) {
const requestId = ++statsRequestId;
const cheat = cheatConfig.value;
const chain = chainConfig.value;
if (!cheat || !chain || !cheat.weth || !chain.harb) {
liquidityStats.value = null;
statsError.value = null;
statsLoading.value = false;
if (notify) {
toast.error("Liquidity helpers not configured for this network");
}
return;
}
if (!rpcUrl.value) {
liquidityStats.value = null;
statsError.value = "Enter an RPC URL to load liquidity stats";
statsLoading.value = false;
if (notify) {
toast.error(statsError.value);
}
return;
}
statsLoading.value = true;
statsError.value = null;
try {
const targetRpcUrl = resolveRpcUrl();
if (!targetRpcUrl) {
throw new Error("RPC URL is empty");
}
const client = createPublicClient({ transport: http(targetRpcUrl) });
const harbAddress = ensureAddress(chain.harb, "KRAIKEN token");
let peripheryManager: Address | null = null;
try {
const [managerFromContract] = await client.readContract({
address: harbAddress,
abi: HarbJson as Abi,
functionName: "peripheryContracts",
}) as [Address, Address];
peripheryManager = managerFromContract;
} catch (error) {
console.warn("[cheats] Unable to read peripheryContracts", error);
}
const overrideManager = (cheat as Record<string, unknown>)?.liquidityManager;
const managerAddressCandidate = typeof overrideManager === "string" && overrideManager.trim().length > 0
? overrideManager
: peripheryManager;
if (!managerAddressCandidate) {
throw new Error("LiquidityManager address not configured for this chain");
}
const liquidityManagerAddress = ensureAddress(managerAddressCandidate, "LiquidityManager");
const wethAddress = ensureAddress(cheat.weth, "WETH token");
const token0isWeth = BigInt(wethAddress) < BigInt(harbAddress);
const factoryAddress = await client.readContract({
address: ensureAddress(cheat.swapRouter, "Uniswap router"),
abi: SWAP_ROUTER_ABI as Abi,
functionName: "factory",
}) as Address;
const poolAddressRaw = await client.readContract({
address: ensureAddress(factoryAddress, "Uniswap factory"),
abi: UNISWAP_FACTORY_ABI as Abi,
functionName: "getPool",
args: token0isWeth ? [wethAddress, harbAddress, POOL_FEE] : [harbAddress, wethAddress, POOL_FEE],
}) as Address;
if (!poolAddressRaw || poolAddressRaw === zeroAddress) {
throw new Error("KRK/WETH pool not found at 1% fee tier");
}
const poolAddress = ensureAddress(poolAddressRaw, "Uniswap pool");
const slot0Response = await client.readContract({
address: poolAddress,
abi: UNISWAP_POOL_ABI as Abi,
functionName: "slot0",
}) as { tick: number } | readonly unknown[];
const currentTick = Array.isArray(slot0Response)
? Number(slot0Response[1])
: Number((slot0Response as { tick: number }).tick);
const [floorRaw, anchorRaw, discoveryRaw] = await Promise.all(
[0, 1, 2].map((stage) =>
client.readContract({
address: liquidityManagerAddress,
abi: LIQUIDITY_MANAGER_POSITIONS_ABI as Abi,
functionName: "positions",
args: [stage],
})
)
);
const toPosition = (raw: any): PositionState => ({
liquidity: BigInt(raw?.liquidity ?? raw?.[0] ?? 0n),
tickLower: Number(raw?.tickLower ?? raw?.[1] ?? 0),
tickUpper: Number(raw?.tickUpper ?? raw?.[2] ?? 0),
});
const floor = toPosition(floorRaw);
const anchor = toPosition(anchorRaw);
const discovery = toPosition(discoveryRaw);
const buyDepthWei = calculateBuyDepth(token0isWeth, currentTick, anchor, discovery);
if (statsRequestId === requestId) {
const positions: PositionView[] = [
{ label: POSITION_STAGE_LABELS[0], ...floor, liquidityFormatted: formatBigInt(floor.liquidity) },
{ label: POSITION_STAGE_LABELS[1], ...anchor, liquidityFormatted: formatBigInt(anchor.liquidity) },
{ label: POSITION_STAGE_LABELS[2], ...discovery, liquidityFormatted: formatBigInt(discovery.liquidity) },
];
liquidityStats.value = {
positions,
currentTick,
poolAddress,
token0isWeth,
buyDepthWei,
buyDepthFormatted: formatEth(buyDepthWei),
};
statsError.value = null;
}
} catch (error: any) {
if (statsRequestId === requestId) {
liquidityStats.value = null;
const message = error?.shortMessage ?? error?.message ?? "Failed to load liquidity stats";
statsError.value = message;
if (notify) {
toast.error(message);
}
}
} finally {
if (statsRequestId === requestId) {
statsLoading.value = false;
if (!statsError.value && notify) {
toast.success("Liquidity stats refreshed");
}
}
}
}
async function refreshLiquidityStats() {
await loadLiquidityStats({ notify: true });
}
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, {
const response = await fetch(resolveRpcUrl(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method, params }),
@ -266,17 +751,20 @@ async function addToken() {
throw new Error("KRAIKEN token address not configured for this chain");
}
const harbAddress = ensureAddress(chain.harb, "KRAIKEN token");
const chainId = resolvedChainId.value;
const provider = (window as any).ethereum;
const [symbol, decimals] = await Promise.all([
readContract(config as any, {
abi: erc20Abi,
address: harbAddress,
functionName: "symbol",
chainId,
}) as Promise<string>,
readContract(config as any, {
abi: erc20Abi,
address: harbAddress,
functionName: "decimals",
chainId,
}) as Promise<number>,
]);
await provider.request({
@ -298,6 +786,50 @@ async function addToken() {
}
}
async function addWethToken() {
if (addingWeth.value || !canAddWeth.value) return;
try {
addingWeth.value = true;
const cheat = cheatConfig.value;
if (!cheat?.weth) {
throw new Error("WETH address not configured for this chain");
}
const wethAddress = ensureAddress(cheat.weth, "WETH token");
const chainId = resolvedChainId.value;
const provider = (window as any).ethereum;
const [symbol, decimals] = await Promise.all([
readContract(config as any, {
abi: erc20Abi,
address: wethAddress,
functionName: "symbol",
chainId,
}) as Promise<string>,
readContract(config as any, {
abi: erc20Abi,
address: wethAddress,
functionName: "decimals",
chainId,
}) as Promise<number>,
]);
await provider.request({
method: "wallet_watchAsset",
params: {
type: "ERC20",
options: {
address: wethAddress,
symbol: symbol?.slice(0, 11) || "WETH",
decimals: Number.isFinite(decimals) ? decimals : 18,
},
},
});
toast.success(`Token request sent (${symbol || "WETH"})`);
} catch (error: any) {
toast.error(error?.message ?? "Failed to add token");
} finally {
addingWeth.value = false;
}
}
async function buyKrk() {
if (!canSwap.value || swapping.value) return;
try {
@ -312,6 +844,7 @@ async function buyKrk() {
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");
@ -321,18 +854,68 @@ async function buyKrk() {
if (amount <= 0n) {
throw new Error("Amount must be greater than zero");
}
const factoryAddress = await readContract(config as any, {
abi: SWAP_ROUTER_ABI,
address: router,
functionName: "factory",
chainId,
}) as Address;
const factory = ensureAddress(factoryAddress, "Uniswap factory");
const poolAddress = await readContract(config as any, {
abi: UNISWAP_FACTORY_ABI,
address: factory,
functionName: "getPool",
args: [weth, harb, 10_000],
chainId,
}) as Address;
if (!poolAddress || poolAddress === zeroAddress) {
throw new Error("No KRK/WETH pool found at 1% fee; deploy and recenter first");
}
const poolLiquidity = await readContract(config as any, {
abi: UNISWAP_POOL_ABI,
address: poolAddress,
functionName: "liquidity",
chainId,
}) as bigint;
if (poolLiquidity === 0n) {
throw new Error("KRK/WETH pool has zero liquidity; run recenter before swapping");
}
const wethBalance = await readContract(config as any, {
abi: erc20Abi,
address: weth,
functionName: "balanceOf",
args: [caller],
chainId,
}) as bigint;
const wrapAmount = amount > wethBalance ? amount - wethBalance : 0n;
if (wrapAmount > 0n) {
await writeContract(config as any, {
abi: WETH_ABI,
address: weth,
functionName: "deposit",
value: amount,
value: wrapAmount,
chainId,
});
}
const allowance = await readContract(config as any, {
abi: erc20Abi,
address: weth,
functionName: "allowance",
args: [caller, router],
chainId,
}) as bigint;
if (allowance < amount) {
await writeContract(config as any, {
abi: erc20Abi,
address: weth,
functionName: "approve",
args: [router, amount],
args: [router, maxUint256],
chainId,
});
}
await writeContract(config as any, {
abi: SWAP_ROUTER_ABI,
address: router,
@ -348,6 +931,7 @@ async function buyKrk() {
sqrtPriceLimitX96: 0n,
},
],
chainId,
});
toast.success("Swap submitted. Watch your wallet for KRK.");
} catch (error: any) {
@ -415,6 +999,23 @@ async function forceRecenter() {
flex-wrap: wrap
gap: 12px
.liquidity-meta
display: flex
flex-wrap: wrap
gap: 12px
font-size: 14px
.liquidity-table
width: 100%
border-collapse: collapse
font-size: 14px
.liquidity-table th,
.liquidity-table td
padding: 6px 8px
text-align: left
border-bottom: 1px solid #2a2a2a
.cheats-hint
font-size: 12px
color: #A3A3A3

View file

@ -12,7 +12,7 @@ import {bigInt2Number, formatBigIntDivision} from "@/utils/helper";
import { computed, ref } from "vue";
import { useStatCollection } from "@/composables/useStatCollection";
import { useStake } from "@/composables/useStake";
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
import { usePositions, type Position } from "@/composables/usePositions";
import { useDark } from "@/composables/useDark";
const { darkTheme } = useDark();

View file

@ -16,7 +16,7 @@
<chart-complete></chart-complete>
<div class="hold-stake-wrapper">
<f-card class="inner-border">
<template v-if="wallet.account.chainId && !chainsArray.includes(wallet.account.chainId)">
<template v-if="!isChainSupported">
Chain not supported
</template>
<template v-else-if="status !== 'connected'">
@ -80,6 +80,7 @@ import { useWallet } from "@/composables/useWallet";
import FCard from "@/components/fcomponents/FCard.vue";
import IconInfo from "@/components/icons/IconInfo.vue";
import FButton from "@/components/fcomponents/FButton.vue";
import { DEFAULT_CHAIN_ID } from "@/config";
// todo interface positions
import { usePositions } from "@/composables/usePositions";
@ -90,8 +91,8 @@ import { compactNumber, InsertCommaNumber } from "@/utils/helper";
const { myActivePositions, tresholdValue, activePositions } = usePositions();
const stats = useStatCollection();
const chains = useChains();
const wallet = useWallet();
const chains = useChains();
function calculateAverageTaxRate(data: any): number {
console.log("data", data);
@ -104,10 +105,10 @@ function calculateAverageTaxRate(data: any): number {
return averageTaxRate * 100;
}
const chainsArray = computed(() => chains.value.map((chain) => chain.id));
const averageTaxRate = computed(() => calculateAverageTaxRate(activePositions.value));
const supportedChainIds = computed(() => chains.value.map((chain) => chain.id));
const currentChainId = computed(() => wallet.account.chainId ?? DEFAULT_CHAIN_ID);
const isChainSupported = computed(() => supportedChainIds.value.includes(currentChainId.value));
onMounted(async () => {});
</script>

View file

@ -1,24 +1,54 @@
import { http, createConfig, createStorage } from "@wagmi/vue";
import { baseSepolia } from "@wagmi/vue/chains";
import { coinbaseWallet, walletConnect } from "@wagmi/vue/connectors";
import { defineChain } from "viem";
const LOCAL_RPC_URL = import.meta.env.VITE_LOCAL_RPC_URL ?? "/rpc/anvil";
const kraikenLocalFork = defineChain({
id: 31337,
name: "Kraiken Local Fork",
network: "kraiken-local",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
default: { http: [LOCAL_RPC_URL] },
public: { http: [LOCAL_RPC_URL] },
},
blockExplorers: {
default: { name: "Local Explorer", url: "" },
},
testnet: true,
});
export const config = createConfig({
chains: [baseSepolia],
chains: [kraikenLocalFork, baseSepolia],
storage: createStorage({ storage: window.localStorage }),
connectors: [
walletConnect({
projectId: "d8e5ecb0353c02e21d4c0867d4473ac5",
metadata: {
name: "Harberg",
description: "Connect your wallet with Harberg",
url: "https://harberg.eth.limo",
name: "Kraiken",
description: "Connect your wallet with Kraiken",
url: "https://kraiken.eth.limo",
icons: [""],
},
}),
coinbaseWallet({ appName: "Harberg", darkMode: true }),
coinbaseWallet({
appName: "Kraiken",
darkMode: true,
preference: {
options: "all",
telemetry: false,
},
}),
],
transports: {
[kraikenLocalFork.id]: http(LOCAL_RPC_URL),
[baseSepolia.id]: http(),
},
});
if (typeof window !== "undefined" && config.state.chainId !== kraikenLocalFork.id) {
config.setState((state) => ({ ...state, chainId: kraikenLocalFork.id }));
}

View file

@ -5,7 +5,10 @@ import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
export default defineConfig(() => {
const localRpcProxyTarget = process.env.VITE_LOCAL_RPC_PROXY_TARGET
return {
// base: "/HarbergPublic/",
plugins: [
vue(),
@ -17,4 +20,20 @@ export default defineConfig({
'kraiken-lib': fileURLToPath(new URL('../kraiken-lib/src', import.meta.url)),
},
},
server: {
proxy: localRpcProxyTarget
? {
'/rpc/anvil': {
target: localRpcProxyTarget,
changeOrigin: true,
secure: false,
rewrite: (path) => {
const rewritten = path.replace(/^\/rpc\/anvil/, '')
return rewritten.length === 0 ? '/' : rewritten
},
},
}
: undefined,
},
}
})