Collection & your position
The collection page is the on-chain explorer for the Tide-LP NFT — the ERC-721 face of a TIDE position. Every whole TIDE you hold materializes one tradable Tide-LP NFT worth 1/10000 of the pool and one live share of every swap fee. This page renders that supply as a gallery of generative, fully on-chain art and turns the connected wallet’s slice of it into a precise, actionable position card.
For the underlying mechanism — why holding is providing liquidity, and why the share count tracks balanceOf(user) / UNIT — see Hold = LP (DN404).
Two views: All tides and My tides
Section titled “Two views: All tides and My tides”A segmented toggle at the top of the page switches between two modes (Mode = "all" | "mine"):
| Tab | What it shows | Wallet required |
|---|---|---|
| All tides | The public gallery of every minted Tide-LP, plus an address-lookup field | No |
| My tides | The connected wallet’s own NFTs, plus the “My position” banner | Yes |
Both modes paginate in pages of 24 cards (PAGE = 24) with a Load more button that shows progress as ids.length / total. Switching tabs resets the page count back to one page.
All tides — the public explorer
Section titled “All tides — the public explorer”By default, All tides renders the entire minted collection: token ids 1 through min(count, totalMinted), where totalMinted comes from the hook’s totalMinted() view.
// ids 1..min(count, totalMinted) — the whole minted galleryconst n = Math.min(count, totalMinted);return Array.from({ length: n }, (_, i) => BigInt(i + 1));Because this is a mixed gallery owned by many addresses, each card is batch-loaded with its owner so it can carry an owner badge. If nothing has been minted yet, the view shows a “No Tide-LP NFTs have been minted yet” empty state.
Look up any address
Section titled “Look up any address”The All tides view exposes a single text field:
Look up any 0x address…
The input is validated with viem’s isAddress:
- An invalid entry leaves the gallery in place and shows the note “That doesn’t look like a valid address.”
- A valid address switches the view away from the full gallery and into that holder’s own collection (loaded via
useOwnedTokens(inspectAddr)), and renders a read-only position card above the grid (MyPositionwithisSelf={false}). - A Clear button resets the field and returns to the full gallery.
My tides — your wallet’s collection
Section titled “My tides — your wallet’s collection”My tides is strictly the connected wallet. If no wallet is connected, the view shows a connect prompt (“Connect your wallet (top right) to see your Tide-LP collection.”). Once connected, it loads the connected address’s owned token ids via useOwnedTokens(address), renders them as cards, and pins the My position banner (MyPosition with isSelf={true}) at the top.
The “My position” banner
Section titled “The “My position” banner”The banner (MyPosition.tsx) reads useUserPosition, useProtocolStats, and usePoolPrice. It surfaces three headline metrics and an earning-status chip:
| Metric | Source | Notes |
|---|---|---|
| TIDE held | balance | The raw ERC-20 balance |
| Tide-LP NFTs | nftBalance | Whole-token positions you currently hold |
| Share of fees | nftBalance / totalShares × 100 | Only shown when nftBalance > 0; otherwise 0 |
The status chip reads Earning (with your position count) when nftBalance > 0, and Not earning yet when you hold only fractional dust.
The exact shortfall to your next NFT
Section titled “The exact shortfall to your next NFT”The banner is clear about one rule of the DN404 model: fractional TIDE earns nothing until it completes a whole token. A whole TIDE mints one NFT and one live fee share; the dust below the next whole token sits idle until you complete it.
The banner computes, to the wei, how much more TIDE you need to mint your next Tide-LP:
const remainder = balance % UNIT; // fractional dust below the next whole tokenconst need = remainder === 0n ? (balance === 0n ? UNIT : 0n) // empty wallet needs a full token; whole balance needs nothing : UNIT - remainder; // otherwise: UNIT − (balance mod UNIT)In words: need = UNIT − (balance mod UNIT), with two edge cases — a zero balance needs a full UNIT for its first NFT, and an exactly-whole balance has no dust (need == 0) and is fully earning. UNIT is 1e18 (1_000_000_000_000_000_000n wei per whole TIDE).
When need > 0, the banner renders:
- A line — “You’re
<need>TIDE from your first / next Tide-LP” (first vs. next is chosen bynftBalance === 0). - A progress meter toward the next whole token, filled by
remainder / 1e18. - A note that one whole TIDE auto-mints a Tide-LP NFT — a
1/10000share of every swap fee — and that fractions don’t earn until they complete a whole token.
When need == 0, it shows an “all set / no idle dust” state instead: every TIDE you hold is whole and earning.
The “Get the last 0.XXXX TIDE” deep-link
Section titled “The “Get the last 0.XXXX TIDE” deep-link”When the card is your own (isSelf), it renders a primary call-to-action that deep-links into the claim dashboard’s swap widget, pre-filled with the ETH amount needed to clear the next whole-token threshold.
The label adapts to your balance:
- Empty wallet → Get TIDE — start your position
- Otherwise → Get the last
<need>TIDE
The CTA grosses need (in TIDE) up into an ETH input using the live pool price (tidePerEth) and the current swap fee, with a small 1.03 buffer so the buy actually clears the whole-token threshold rather than landing just short of it:
// best-effort prefill: TIDE needed → ETH, grossed up for the live fee + 3% bufferconst ethNeeded = ((Number(need) / 1e18 / tidePerEth) / (1 - feeFrac)) * 1.03;const buyHref = ethStr ? `/claim/?buy=${ethStr}` : "/claim/";The resulting link is /claim/?buy=<eth>. The swap widget on the claim page reads the ?buy= query param on mount and prefills the buy amount with it. See Claim dashboard for the swap widget.
NFT cards
Section titled “NFT cards”Each card (NftPlate.tsx) is a “plate” rendering the on-chain Tide-LP art:
- The image is the contract’s generative SVG (
meta.image), decoded from the NFT’s data-URItokenURI. The art is deterministic,pure, and rendered entirely on-chain — no IPFS, no server. See TideArt (on-chain art). - The art links to the NFT on the block explorer via
explorer.nft(ADDRESSES.mirror, tokenId)— i.e. the token on the mirror contract. - A serif title (
meta.name, e.g.Tide #1) and up to four trait chips (meta.attributes, sliced to four) — drawn from the on-chain traitsHue,Amplitude,Period,Phase,Harmonics. - A null/undecodable
metarenders a placeholder card labelled “burned or unreachable” — expected for ids that have since been burned (a sell burns NFTs) or that the RPC could not return.
Owner badges
Section titled “Owner badges”An owner address badge is rendered only in the mixed gallery (All tides, with no address lookup active). It links to the owner on the explorer (explorer.address(owner)). A single holder’s grid — your own collection, or a looked-up address — is entirely owned by that one address, so no per-card owner badge is shown there.
This is why the batch loader is told whether to fetch owners at all: it requests owner data only for the gallery.
How the page reads the chain
Section titled “How the page reads the chain”The collection page is read-only and decoupled from the wallet — the gallery and any address lookup work before you connect. It avoids any indexer by fanning reads out through batched multicall:
useProtocolStatssuppliestotalMinted(the gallery’s upper bound) andtotalShares(the fee-share denominator).useOwnedTokens(holder)returns a single holder’s owned token-id array; the gallery passesundefinedto skip it.useNftBatch(ids, withOwner)decodes each token’s metadata (image + traits) for the visible ids, and — whenwithOwneris true (gallery mode only) — also returns each token’s owner in the same batch. Decoded metadata is cached by token id, so paging and re-rendering don’t re-fetch.
Pagination is purely client-side over the loaded ids window: Load more widens the window by another PAGE and the batch loader fetches the newly-visible ids.