diff --git a/.gitignore b/.gitignore index 7386ffe..68ec8bc 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ onchain/node_modules/ ponder-repo tmp foundry.lock +services/ponder/.env.local diff --git a/AGENTS.md b/AGENTS.md index 9cfb75f..4001874 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`. diff --git a/scripts/local_env.sh b/scripts/local_env.sh index d023255..bb957df 100755 --- a/scripts/local_env.sh +++ b/scripts/local_env.sh @@ -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,8 +173,15 @@ ensure_dependencies() { start_anvil() { if [[ -f "$ANVIL_PID_FILE" ]]; then - log "Anvil already running (pid $(cat "$ANVIL_PID_FILE"))" - return + 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)" @@ -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" diff --git a/services/ponder/.env.local b/services/ponder/.env.local deleted file mode 100644 index 78eb102..0000000 --- a/services/ponder/.env.local +++ /dev/null @@ -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 diff --git a/services/ponder/src/api/index.ts b/services/ponder/src/api/index.ts index 4057024..90b8d0e 100644 --- a/services/ponder/src/api/index.ts +++ b/services/ponder/src/api/index.ts @@ -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 })); @@ -12,4 +21,4 @@ app.use("/sql/*", client({ db, schema })); app.use("/graphql", graphql({ db, schema })); app.use("/", graphql({ db, schema })); -export default app; \ No newline at end of file +export default app; diff --git a/services/ponder/src/helpers/stats.ts b/services/ponder/src/helpers/stats.ts index 637137b..22db85f 100644 --- a/services/ponder/src/helpers/stats.ts +++ b/services/ponder/src/helpers/stats.ts @@ -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 (fn: () => Promise, fallback: T, label: string): Promise => { + try { + return await fn(); + } catch (error) { + console.warn(`[stats.ensureStatsExists] Falling back for ${label}`, error); + return fallback; + } + }; + const [kraikenTotalSupply, stakeTotalSupply, outstandingStake] = await Promise.all([ - client.readContract({ - abi: contracts.Kraiken.abi, - address: contracts.Kraiken.address, - functionName: "totalSupply", - }), - client.readContract({ - abi: contracts.Stake.abi, - address: contracts.Stake.address, - functionName: "totalSupply", - }), - client.readContract({ - abi: contracts.Stake.abi, - address: contracts.Stake.address, - functionName: "outstandingStake", - }), + 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; diff --git a/web-app/src/components/StakeHolder.vue b/web-app/src/components/StakeHolder.vue index 943a001..0667eca 100644 --- a/web-app/src/components/StakeHolder.vue +++ b/web-app/src/components/StakeHolder.vue @@ -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; } diff --git a/web-app/src/components/chart/ChartComplete.vue b/web-app/src/components/chart/ChartComplete.vue index 71ec2bc..8af4a66 100644 --- a/web-app/src/components/chart/ChartComplete.vue +++ b/web-app/src/components/chart/ChartComplete.vue @@ -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(); diff --git a/web-app/src/composables/usePositions.ts b/web-app/src/composables/usePositions.ts index 47d6a05..1c8d12a 100644 --- a/web-app/src/composables/usePositions.ts +++ b/web-app/src/composables/usePositions.ts @@ -15,6 +15,12 @@ import logger from "@/utils/logger"; const rawActivePositions = ref>([]); const rawClosedPositoins = ref>([]); const loading = ref(false); +const positionsError = ref(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; } - 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, }; } diff --git a/web-app/src/composables/useStatCollection.ts b/web-app/src/composables/useStatCollection.ts index 761c749..8640132 100644 --- a/web-app/src/composables/useStatCollection.ts +++ b/web-app/src/composables/useStatCollection.ts @@ -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>([]); const loading = ref(false); const initialized = ref(false); +const statsError = ref(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; + } - loading.value = false; - initialized.value = true; + 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, }); } diff --git a/web-app/src/composables/useWallet.ts b/web-app/src/composables/useWallet.ts index b3296c9..b09c3db 100644 --- a/web-app/src/composables/useWallet.ts +++ b/web-app/src/composables/useWallet.ts @@ -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({ symbol: "", formatted: "" }); -const account = ref({ - address: undefined, - addresses: undefined, - chain: undefined, - chainId: undefined, - connector: undefined, - isConnected: false, - isConnecting: false, - isDisconnected: true, - isReconnecting: false, - status: "disconnected", -}); +const account = ref(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); diff --git a/web-app/src/config.ts b/web-app/src/config.ts index 965b22f..e710453 100644 --- a/web-app/src/config.ts +++ b/web-app/src/config.ts @@ -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, diff --git a/web-app/src/views/CheatsView.vue b/web-app/src/views/CheatsView.vue index e5b8a2e..f5364fd 100644 --- a/web-app/src/views/CheatsView.vue +++ b/web-app/src/views/CheatsView.vue @@ -21,10 +21,66 @@ @click="addToken" variant="secondary" >{{ addingToken ? "Adding…" : "Add KRK token" }} + {{ addingWeth ? "Adding…" : "Add WETH token" }} + +
+ + + {{ statsLoading ? "Refreshing…" : "Refresh stats" }} +
+
+
@@ -65,7 +121,7 @@ diff --git a/web-app/src/wagmi.ts b/web-app/src/wagmi.ts index eec377b..beaad4f 100644 --- a/web-app/src/wagmi.ts +++ b/web-app/src/wagmi.ts @@ -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 })); +} diff --git a/web-app/vite.config.ts b/web-app/vite.config.ts index 4ed8f2d..55ddfb0 100644 --- a/web-app/vite.config.ts +++ b/web-app/vite.config.ts @@ -5,16 +5,35 @@ 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(), - vueDevTools(), - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - 'kraiken-lib': fileURLToPath(new URL('../kraiken-lib/src', import.meta.url)), + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '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, + }, + } })