fix: seed campaign data when no live vault is reachable
All checks were successful
deploy / deploy (push) Successful in 54s

The public build sets ACTIVE_CHAIN_ID to Arbitrum Sepolia (421614), which has
no deployment and isn't in the prod wagmi config, so useReadContracts never
resolved -> Campaigns/Markets/Positions hung on skeletons forever.

Add CAMPAIGN_SEED (mirrors script/Deploy.s.sol's launch set) and HAS_LIVE_VAULT.
When no vault is reachable, useCampaigns returns the seeded launch set instead
of reading on-chain, and the position/balance reads are disabled so they can't
hang against an unreachable chain. Live anvil/dev path is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Денис Зефиров 2026-07-11 23:25:06 +04:00
parent 24fb890971
commit 68baf5704b
3 changed files with 77 additions and 5 deletions

View file

@ -2,6 +2,8 @@ import { useAccount, useReadContracts } from "wagmi";
import { hoodquidityVaultsAbi } from "@/lib/abi/HoodquidityVaults";
import {
ACTIVE_CHAIN_ID,
CAMPAIGN_SEED,
HAS_LIVE_VAULT,
getActiveDeployment,
USD_DECIMALS,
type MarketMeta,
@ -30,6 +32,35 @@ export function rewardsRemainingUsd(c: CampaignState, nowSec: number): number {
const scale = 10 ** USD_DECIMALS;
/** How far into the launch schedule the seeded campaigns sit: 2d 4h, so a
* 14-day campaign reads ~11d 20h remaining, past the 1-day boost window. */
const SEED_ELAPSED_SEC = 187_200;
/** Build a campaign row from the static seed, for the no-live-vault build. */
function seedCampaign(meta: MarketMeta, now: number): CampaignState {
const s = CAMPAIGN_SEED[meta.campaignId];
const start = now - SEED_ELAPSED_SEC;
const end = start + s.durationDays * 86_400;
const boostWindow = s.boostWindowDays * 86_400;
let status: CampaignState["status"] = "active";
if (now < start) status = "upcoming";
else if (now < start + boostWindow) status = "boost";
else if (now >= end) status = "ended";
return {
meta,
tvlUsd: s.tvlUsd,
targetDepthUsd: s.targetDepthUsd,
rewardBudgetUsd: s.rewardBudgetUsd,
start,
end,
boostWindow,
boostBps: s.boostBps,
durationDays: s.durationDays,
activeLPs: s.activeLPs,
status,
};
}
export function useCampaigns() {
const deployment = getActiveDeployment();
@ -41,10 +72,21 @@ export function useCampaigns() {
args: [BigInt(m.campaignId)],
chainId: ACTIVE_CHAIN_ID,
})),
query: { refetchInterval: 15_000 },
query: { enabled: HAS_LIVE_VAULT, refetchInterval: 15_000 },
});
const now = Math.floor(Date.now() / 1000);
// Showcase build with no reachable vault: render the seeded launch set.
if (!HAS_LIVE_VAULT) {
return {
campaigns: deployment.markets.map((m) => seedCampaign(m, now)),
isLoading: false,
error: null,
deployment,
};
}
const campaigns: CampaignState[] | undefined = data?.map((res, i) => {
const meta = deployment.markets[i];
const c = res.result as
@ -122,7 +164,7 @@ export function usePositions() {
chainId: ACTIVE_CHAIN_ID,
} as const,
]),
query: { enabled: !!address, refetchInterval: 15_000 },
query: { enabled: HAS_LIVE_VAULT && !!address, refetchInterval: 15_000 },
});
const positions = deployment.markets.map((meta, i) => {
@ -174,7 +216,7 @@ export function usePosition(campaignId: number) {
chainId: ACTIVE_CHAIN_ID,
},
],
query: { enabled: !!address, refetchInterval: 15_000 },
query: { enabled: HAS_LIVE_VAULT && !!address, refetchInterval: 15_000 },
});
const raw = data?.[0]?.result as

View file

@ -98,6 +98,36 @@ const DEPLOYMENTS: Record<number, Deployment | null> = {
*/
export const ACTIVE_CHAIN_ID = import.meta.env.DEV ? 31337 : 421614;
/**
* True only when a real vault contract is reachable on the active chain
* (anvil in dev). The public showcase build has no live vault yet, so the
* campaign hooks fall back to the seed below instead of hanging on reads.
*/
export const HAS_LIVE_VAULT = DEPLOYMENTS[ACTIVE_CHAIN_ID] != null;
export interface CampaignSeed {
targetDepthUsd: number;
rewardBudgetUsd: number;
durationDays: number;
boostWindowDays: number;
boostBps: number;
tvlUsd: number;
activeLPs: number;
}
/**
* Seed campaign parameters mirroring script/Deploy.s.sol's launch set. Used
* when HAS_LIVE_VAULT is false so the showcase renders realistic campaigns
* without an on-chain vault. Keyed by campaignId.
*/
export const CAMPAIGN_SEED: Record<number, CampaignSeed> = {
0: { targetDepthUsd: 1_000_000, rewardBudgetUsd: 50_000, durationDays: 14, boostWindowDays: 1, boostBps: 15_000, tvlUsd: 5_000, activeLPs: 1 },
1: { targetDepthUsd: 750_000, rewardBudgetUsd: 40_000, durationDays: 14, boostWindowDays: 1, boostBps: 15_000, tvlUsd: 0, activeLPs: 0 },
2: { targetDepthUsd: 500_000, rewardBudgetUsd: 25_000, durationDays: 21, boostWindowDays: 1, boostBps: 15_000, tvlUsd: 0, activeLPs: 0 },
3: { targetDepthUsd: 2_000_000, rewardBudgetUsd: 60_000, durationDays: 30, boostWindowDays: 1, boostBps: 15_000, tvlUsd: 0, activeLPs: 0 },
4: { targetDepthUsd: 400_000, rewardBudgetUsd: 35_000, durationDays: 14, boostWindowDays: 1, boostBps: 15_000, tvlUsd: 0, activeLPs: 0 },
};
export function getActiveDeployment(): Deployment {
return DEPLOYMENTS[ACTIVE_CHAIN_ID] ?? local;
}

View file

@ -9,7 +9,7 @@ import {
import { useAppKit } from "@reown/appkit/react";
import { hoodquidityVaultsAbi } from "@/lib/abi/HoodquidityVaults";
import { mockERC20Abi } from "@/lib/abi/MockERC20";
import { ACTIVE_CHAIN_ID, getActiveDeployment, USD_DECIMALS } from "@/lib/contracts";
import { ACTIVE_CHAIN_ID, HAS_LIVE_VAULT, getActiveDeployment, USD_DECIMALS } from "@/lib/contracts";
import { useCampaigns, usePositions, type CampaignState } from "@/hooks/useCampaigns";
import { useAllMarketData } from "@/hooks/useMarketData";
import { feeApr, rewardApr } from "@/lib/math";
@ -91,7 +91,7 @@ export default function AppBoard() {
chainId: ACTIVE_CHAIN_ID,
},
],
query: { enabled: !!address },
query: { enabled: HAS_LIVE_VAULT && !!address },
});
const balance = (walletData?.[0]?.result as bigint | undefined) ?? 0n;
const allowance = (walletData?.[1]?.result as bigint | undefined) ?? 0n;