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

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;
}
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;
}
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,
});
}

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);