Skip to content

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

A segmented toggle at the top of the page switches between two modes (Mode = "all" | "mine"):

TabWhat it showsWallet required
All tidesThe public gallery of every minted Tide-LP, plus an address-lookup fieldNo
My tidesThe connected wallet’s own NFTs, plus the “My position” bannerYes

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.

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 gallery
const 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.

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 (MyPosition with isSelf={false}).
  • A Clear button resets the field and returns to the full gallery.

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 banner (MyPosition.tsx) reads useUserPosition, useProtocolStats, and usePoolPrice. It surfaces three headline metrics and an earning-status chip:

MetricSourceNotes
TIDE heldbalanceThe raw ERC-20 balance
Tide-LP NFTsnftBalanceWhole-token positions you currently hold
Share of feesnftBalance / totalShares × 100Only 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 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 token
const 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 by nftBalance === 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/10000 share 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.

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% buffer
const 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.

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-URI tokenURI. 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 traits Hue, Amplitude, Period, Phase, Harmonics.
  • A null/undecodable meta renders 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.

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.

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:

  • useProtocolStats supplies totalMinted (the gallery’s upper bound) and totalShares (the fee-share denominator).
  • useOwnedTokens(holder) returns a single holder’s owned token-id array; the gallery passes undefined to skip it.
  • useNftBatch(ids, withOwner) decodes each token’s metadata (image + traits) for the visible ids, and — when withOwner is 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.