fix(market): /genesis printed a tier table no contract backs — read it instead

The page rendered four hard-coded tiers claiming 10x1B + 100x100M +
1000x10M + 10000x1M LUX "permanently locked", plus per-tier "10x/5x/2x/1x
rewards". None of it came from anywhere:

- The collection 0x004287c47efc912fec391979154454a8017a76c6 has NO CODE at
  head. Measured on https://api.lux.network/v1/bc/C/rpc at block 1098191:
  eth_getCode -> "0x", totalSupply() -> "0x" (revert). Positive control
  WLUX 0x4888e4a2.. -> 3462 hex chars and a live totalSupply.
- The supplies contradicted the tokenomics reference: NFT-COLLECTION.md
  has Genesis Validator count 100 (not 10) and records that the
  Validator/Mini/Nano hierarchy is "tokenomics-designed but not issued".
- GenesisNFTs.sol has no reward-multiplier concept at all. The 10x/5x/2x/1x
  figures were invented outright.

Now the page calls totalMinted() and totalLuxLocked() — both real reads,
GenesisNFTs.sol:829 and :214 — and renders whatever chain returns. When the
call reverts, as it does today, it says supply and bonded LUX are
unavailable and names the address, rather than substituting a number.

Also drops src/lib/wagmi.ts: a second createConfig that nothing imported,
shadowing the live one in app/providers.tsx.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-27 14:10:00 -07:00
co-authored by Hanzo Dev
parent 6c5ece0bb3
commit 9e56f246d0
3 changed files with 113 additions and 83 deletions
+87 -70
View File
@@ -1,96 +1,113 @@
'use client'
import { formatUnits } from 'viem'
import { useReadContracts } from 'wagmi'
import { ArrowRight, ExternalLink } from 'lucide-react'
import { Header } from '@/components/Header'
import { Card } from '@/components/ui/card'
import { ArrowRight } from 'lucide-react'
import { useChainContext } from '@/hooks/useChain'
import { CHAIN_INFO, explorerUrl } from '@/lib/chains'
import { GENESIS_ABI, GENESIS_NFT } from '@/lib/contracts'
const GENESIS_TIERS = [
{
name: 'Genesis',
lockedLux: '1,000,000,000',
description:
'The pinnacle of the Lux Genesis Collection. Each Genesis NFT permanently locks 1 billion LUX, with all staking rewards flowing directly to the holder.',
maxSupply: 10,
rewardMultiplier: '10x',
},
{
name: 'Validator',
lockedLux: '100,000,000',
description:
'Validator-tier Genesis NFTs lock 100M LUX permanently. Holders receive validator staking rewards through the ValidatorVault and LiquidLUX (xLUX) system.',
maxSupply: 100,
rewardMultiplier: '5x',
},
{
name: 'Mini',
lockedLux: '10,000,000',
description:
'Mini Genesis NFTs lock 10M LUX permanently. A more accessible entry point to Genesis rewards with proportional staking returns.',
maxSupply: 1000,
rewardMultiplier: '2x',
},
{
name: 'Nano',
lockedLux: '1,000,000',
description:
'Nano Genesis NFTs lock 1M LUX permanently. The most accessible Genesis tier, providing base-level staking rewards to holders.',
maxSupply: 10000,
rewardMultiplier: '1x',
},
]
/** One tile. `value` is always a measurement; there is no default to fall back on. */
function Stat({ label, value }: { label: string; value: string }) {
return (
<Card className="p-5">
<div className="text-[13px] text-muted-foreground mb-1">{label}</div>
<div className="text-2xl font-bold font-mono">{value}</div>
</Card>
)
}
export default function GenesisPage() {
const { chainId } = useChainContext()
const chain = CHAIN_INFO[chainId]
const address = GENESIS_NFT[chainId]
const { data, isLoading, isError } = useReadContracts({
allowFailure: false,
contracts: [
{ chainId, address, abi: GENESIS_ABI, functionName: 'totalMinted' },
{ chainId, address, abi: GENESIS_ABI, functionName: 'totalLuxLocked' },
],
query: { enabled: Boolean(address) },
})
const [minted, luxLocked] = data ?? []
return (
<div>
<Header />
<main className="max-w-[1200px] mx-auto px-6 py-8">
<section className="text-center mb-12">
<section className="text-center mb-10">
<h1 className="text-4xl font-bold mb-3">Genesis NFTs</h1>
<p className="text-base text-muted-foreground max-w-[600px] mx-auto">
Permanently locked LUX tokens with staking rewards flowing to NFT holders. Genesis NFTs control validator
staking returns through the ValidatorVault and LiquidLUX (xLUX) system.
Each Genesis NFT bonds LUX permanently and routes the validator staking rewards on that
bond to whoever holds the NFT. Both figures below are read from the collection contract
on every page load.
</p>
</section>
{/* Reward flow diagram */}
<Card className="p-6 mb-12 text-center">
<h3 className="text-base font-semibold mb-4">Reward Flow</h3>
{!address ? (
<Card className="p-6 text-center text-muted-foreground">
Genesis NFTs are a Lux Network collection. No Genesis contract is registered for{' '}
{chain?.name ?? `chain ${chainId}`}.
</Card>
) : isLoading ? (
<Card className="p-6 text-center text-muted-foreground">
Reading the Genesis contract on {chain?.name ?? `chain ${chainId}`}…
</Card>
) : isError || data === undefined ? (
<Card className="p-6 space-y-3">
<div className="font-semibold">Supply and bonded LUX are unavailable.</div>
<p className="text-sm text-muted-foreground">
The Genesis collection is registered at{' '}
<code className="font-mono text-xs">{address}</code> on{' '}
{chain?.name ?? `chain ${chainId}`}, but the contract does not answer:{' '}
<code className="font-mono text-xs">eth_getCode</code> returns{' '}
<code className="font-mono text-xs">0x</code> and every view call reverts, so there is
no minted count and no bonded balance to show. Nothing is reported here until the
contract answers again.
</p>
<a
className="inline-flex items-center gap-1.5 text-sm hover:underline"
href={`${explorerUrl(chainId)}/address/${address}`}
target="_blank"
rel="noreferrer"
>
Inspect the address <ExternalLink className="h-3.5 w-3.5" />
</a>
</Card>
) : (
<div className="grid grid-cols-[repeat(auto-fit,minmax(240px,1fr))] gap-4">
<Stat label="Minted" value={minted!.toLocaleString()} />
<Stat
label="LUX bonded (permanent)"
value={Number(formatUnits(luxLocked!, 18)).toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
/>
</div>
)}
<Card className="p-6 mt-10 text-center">
<h3 className="text-base font-semibold mb-4">How rewards reach a holder</h3>
<div className="flex items-center justify-center gap-4 flex-wrap text-sm">
<span>Validator Staking</span>
<span>Validator staking</span>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
<span>ValidatorVault.depositRewards()</span>
<span className="font-mono text-xs">ValidatorVault.depositRewards()</span>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
<span>LiquidLUX.depositValidatorRewards()</span>
<span className="font-mono text-xs">LiquidLUX.depositValidatorRewards()</span>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
<span className="font-semibold">xLUX holders</span>
</div>
<p className="text-[13px] text-muted-foreground mt-4">
The call path above is the design in{' '}
<code className="font-mono text-xs">standard/contracts/nft/GenesisNFTs.sol</code>. It is
not a claim about rewards paid — no reward figure appears on this page unless it was
read from chain.
</p>
</Card>
{/* Tier grid */}
<div className="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-6">
{GENESIS_TIERS.map((tier) => (
<Card key={tier.name} className="overflow-hidden">
<div className="aspect-[4/3] bg-gradient-to-br from-secondary to-muted flex flex-col items-center justify-center gap-2">
<span className="text-xs text-muted-foreground uppercase tracking-[2px]">
Genesis
</span>
<span className="text-4xl font-bold">{tier.name}</span>
<span className="text-sm text-primary">{tier.rewardMultiplier} rewards</span>
</div>
<div className="p-5">
<div className="text-[13px] text-muted-foreground mb-1">Permanently Locked</div>
<div className="text-2xl font-bold mb-3 font-mono">{tier.lockedLux} LUX</div>
<p className="text-[13px] text-muted-foreground leading-relaxed mb-4">
{tier.description}
</p>
<div className="flex justify-between text-[13px] text-muted-foreground border-t border-border pt-3">
<span>Max Supply: {tier.maxSupply.toLocaleString()}</span>
<span>Rewards: {tier.rewardMultiplier}</span>
</div>
</div>
</Card>
))}
</div>
</main>
</div>
)
+26
View File
@@ -26,6 +26,32 @@ export const WRAPPED_NATIVE: Record<number, `0x${string}`> = {
96369: '0x4888E4a2Ee0F03051c72D2BD3ACf755eD3498B3E',
}
/**
* Genesis NFT collection per chain — standard/contracts/nft/GenesisNFTs.sol.
*
* Unlike CONTRACTS above, an address here does not enable a transaction; it
* only tells /genesis where to probe. The probe is the point: this address is
* where the indexer recorded 118 mints on 2026-07-09 (block 1085879), but it
* has NO CODE at head. Measured against https://api.lux.network/v1/bc/C/rpc at
* block 1098191, with WLUX 0x4888e4a2.. as the positive control:
*
* 0x004287c4..76c6 eth_getCode -> "0x" totalSupply() -> "0x" (revert)
* 0x4888e4a2..8b3e eth_getCode -> 3462 totalSupply() -> 159126795.518e18
*
* So /genesis reads totalMinted()/totalLuxLocked() live and reports the read as
* unavailable when it reverts, instead of printing the tier table that used to
* be hard-coded into the page.
*/
export const GENESIS_NFT: Record<number, `0x${string}`> = {
96369: '0x004287c47efc912fec391979154454a8017a76c6',
}
/** GenesisNFTs.sol reads: totalMinted():829, totalLuxLocked():214 (public state). */
export const GENESIS_ABI = [
{ type: 'function', name: 'totalMinted', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
{ type: 'function', name: 'totalLuxLocked', stateMutability: 'view', inputs: [], outputs: [{ type: 'uint256' }] },
] as const
export const ERC721_ABI = [
{ type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }], outputs: [{ type: 'uint256' }] },
{ type: 'function', name: 'ownerOf', stateMutability: 'view', inputs: [{ name: 'tokenId', type: 'uint256' }], outputs: [{ type: 'address' }] },
-13
View File
@@ -1,13 +0,0 @@
import { createConfig, http } from 'wagmi'
import { luxMainnet, zooMainnet, hanzoMainnet, spcMainnet, parsMainnet } from './chains'
export const config = createConfig({
chains: [luxMainnet, zooMainnet, hanzoMainnet, spcMainnet, parsMainnet],
transports: {
[luxMainnet.id]: http(),
[zooMainnet.id]: http(),
[hanzoMainnet.id]: http(),
[spcMainnet.id]: http(),
[parsMainnet.id]: http(),
},
})