Skip to content

fix(my-markets): scope creator ownership scan lifecycle to the active wallet - #2426

Open
Bayyan16 wants to merge 1 commit into
dcccrypto:playgroundfrom
Bayyan16:fix/my-markets-creator-scan-lifecycle-2425
Open

fix(my-markets): scope creator ownership scan lifecycle to the active wallet#2426
Bayyan16 wants to merge 1 commit into
dcccrypto:playgroundfrom
Bayyan16:fix/my-markets-creator-scan-lifecycle-2425

Conversation

@Bayyan16

Copy link
Copy Markdown
Contributor

Summary

Fixes #2425.

This PR makes the /my-markets creator ownership lifecycle explicitly wallet-scoped and prevents pending, failed, or stale ownership scans from being rendered as successfully verified results.

useCreatedMarkets() performs two complementary creator-detection paths:

  1. Direct authority matching for incomplete launches whose authority has not yet rotated.
  2. LP-portfolio ownership scanning for completed launches whose marketauth has rotated to a stake-pool PDA.

Previously, the LP ownership scan ran asynchronously outside the lifecycle exposed by the hook. The hook returned only the general discovery loading and error state, while creator results were stored in a shared array without identifying the wallet or scan that produced them.

That allowed /my-markets to expose three incorrect states:

  • a confirmed-looking empty state while creator ownership verification was still pending;
  • a confirmed-looking empty state after the ownership RPC scan failed;
  • creator markets from wallet A after the active wallet had already switched to wallet B.

This PR introduces a wallet-scoped creator scan state, propagates its loading and error lifecycle, prevents stale cross-wallet results, and avoids redundant RPC scans when discovery returns a new array with identical content.


Problem

The previous hook contract returned:

return {
  myMarkets: enrichedMarkets,
  loading: discoveryLoading,
  error,
  connected: !!publicKey,
  refetch: discoveryRefetch,
};

However, creator discovery was not complete when discoveryLoading became false.

The hook still needed to perform:

connection.getProgramAccounts(...)

to recover completed creator markets through creator-owned LP portfolios.

Because that asynchronous ownership scan was not represented in the returned state, the page could interpret an unresolved or failed scan as a final result.

Previous lifecycle

General market discovery completes
→ discoveryLoading becomes false
→ LP creator ownership scan is still pending
→ hook returns loading = false
→ myMarkets may still be empty or belong to the previous wallet
→ /my-markets renders empty or stale content

Ownership scan failures were also caught without being surfaced through the hook error contract, making these two conditions indistinguishable:

Ownership verification succeeded and found no markets
Ownership verification failed before producing a result

Root Cause

1. Creator ownership scanning was not included in loading state

The hook exposed only:

loading: discoveryLoading

The asynchronous LP-portfolio scan could therefore remain pending after the hook had already reported that loading was complete.


2. Ownership RPC failures were not exposed

A rejected getProgramAccounts() request did not contribute to the returned error state.

The page could therefore render an empty result with:

loading = false
error = null
myMarkets = []

even though creator ownership verification had failed.


3. Creator results were not associated with their originating wallet

Creator markets were stored in a shared state array without a wallet key or scan identity.

After:

Wallet A → Wallet B

the already committed wallet A results remained available while the wallet B scan was pending.

The existing cancellation guard prevented some late asynchronous writes, but it did not invalidate results that had already been committed for the previous wallet.


4. Discovery array replacements could trigger duplicate scans

Market discovery can return a new array reference while preserving the exact same:

  • wallet;
  • slab addresses;
  • program IDs;
  • market authorities;
  • collateral mints.

Depending directly on the markets array reference caused an unnecessary ownership RPC scan for content-equivalent discovery results.

This could increase RPC load and replace a verified result with an error from a redundant scan.


Implementation

Wallet-scoped creator scan state

The shared creator market array has been replaced with an explicit lifecycle model:

type CreatorScanState = {
  key: string | null;
  status: "idle" | "loading" | "success" | "error";
  slabAddresses: string[];
  labels: Record<string, string>;
  error: string | null;
};

The state now records:

  • the scan identity;
  • the current lifecycle status;
  • verified slab identities;
  • resolved labels;
  • scan-specific error information.

Content-derived scan identity

The creator scan key is derived from the inputs that materially define creator verification:

connected wallet
program ID set
market/slab set
market authority
collateral mint
manual refresh generation

This ensures that results are valid only for the exact wallet and market identity that produced them.

A changed wallet, market set, program set, or refresh generation produces a new scan key.


Render-time stale-result gating

Creator results are exposed only when:

creatorScanState.key === creatorScanKey

and the matching scan has completed successfully.

This immediately withholds wallet A results when wallet B becomes active, even before the new effect finishes executing.

The fix does not rely only on calling setLoading(true) from an effect, because effects run after render and could otherwise allow a transient stale render.


Creator scan loading lifecycle

The hook now treats creator verification as loading when:

  • a current scan key exists;
  • no result for that key has been committed;
  • the matching scan is idle or pending.

The returned loading contract is now:

loading: discoveryLoading || creatorScanLoading

As a result, /my-markets cannot render a confirmed empty state before creator ownership verification finishes.


Creator scan error propagation

Ownership RPC failures now produce a scan-specific error state instead of being converted into a verified empty result.

The returned error contract is:

error: discoveryError ?? creatorScanError

A failed ownership scan therefore reaches the page's existing recoverable error path.

No /my-markets page change is required because the page already renders states in the correct order:

loading
→ error
→ empty
→ creator markets

The issue was in the hook contract, not the page state ordering.


Complete RPC verification

Configured program scans are executed through Promise.all().

If one required program ownership scan fails, the overall creator result is not committed as a successful partial or empty result.

This prevents incomplete verification from being presented as authoritative.

Individually malformed portfolio accounts remain safely ignored without creating a creator match.


Authority and LP-portfolio detection preserved

The patch preserves both existing creator detection paths:

Incomplete launch

A market remains identifiable when the connected wallet still matches:

configV17.marketauth
or
header.admin

Completed launch

After authority rotation, the creator market is recovered through a creator-owned LP portfolio returned by getProgramAccounts().

Trading-only portfolios continue to be excluded through the existing isLpPortfolio() guard.

This PR does not broaden creator ownership detection to trading participants.


Stale async completion protection

Each ownership scan captures its active scan key and market snapshot.

A completion is committed only while the scan remains active. Effect cleanup marks superseded executions as cancelled, preventing older wallet or market-set scans from overwriting newer state.


Same-content discovery stability

The latest discovery objects are stored in a ref:

const creatorScanMarketsRef = useRef(markets);

The creator ownership effect no longer depends directly on the raw markets array reference.

Instead, execution is controlled by the content-derived scan key.

This means:

same wallet
same slabs
same program IDs
same authorities
same collateral mints
new array reference

does not trigger another getProgramAccounts() request.

Verified slab identities are still merged with the latest discovery objects, so the hook does not retain stale market object snapshots.


Refresh behavior

The returned refetch() now:

  1. increments the creator scan generation;
  2. immediately invalidates the currently verified scan identity;
  3. triggers general market discovery refresh;
  4. causes creator ownership verification to run again for the new generation.

This ensures manual refresh covers both discovery layers.


Behavior Before

Pending ownership scan

discoveryLoading = false
ownership scan = pending
returned loading = false
page may render empty state

Failed ownership scan

getProgramAccounts rejects
error is swallowed
returned error = null
page renders confirmed-looking empty state

Wallet switch

wallet A markets loaded
wallet changes to B
wallet B scan starts
wallet A markets remain visible

Same-content discovery replacement

discovery returns equivalent new array
ownership effect runs again
duplicate RPC scan occurs

Behavior After

Pending ownership scan

discoveryLoading = false
ownership scan = pending
creatorScanLoading = true
returned loading = true
page remains in loading state

Failed ownership scan

getProgramAccounts rejects
creatorScanState.status = error
creatorScanError is returned
page renders recoverable error state

Wallet switch

wallet A markets loaded
wallet changes to B
scan key changes immediately
wallet A results no longer match active key
stale results are withheld
wallet B remains loading until verified

Same-content discovery replacement

discovery returns equivalent new array
content-derived scan key remains unchanged
no duplicate RPC scan is executed
verified creator result remains available

Regression Tests

Four regression tests were added to cover the lifecycle corrected by this PR.

1. Pending creator ownership scan keeps the hook loading

keeps loading while creator ownership scan is pending

Verifies that loading remains true after market discovery has finished but while getProgramAccounts() is still unresolved.


2. Creator ownership RPC failures are surfaced

surfaces creator ownership scan failures instead of confirming an empty result

Verifies that an ownership RPC rejection produces a non-null error instead of returning a successful-looking empty result.


3. Previous-wallet markets are withheld

withholds wallet A markets while wallet B ownership scan is pending

Verifies that wallet A markets are immediately hidden after wallet B becomes active and before wallet B ownership verification completes.


4. Same-content discovery replacements do not rescan

keeps a verified creator snapshot stable across same-content discovery replacements

Verifies that replacing the discovery array with content-equivalent data:

  • does not execute another ownership RPC scan;
  • preserves the verified creator snapshot;
  • does not introduce an ownership error.

Existing Behavior Preserved

The existing regression suite confirms that the patch continues to:

  • return an empty result when no wallet is connected;
  • return an empty result when no markets are discovered;
  • identify directly administered markets;
  • exclude markets administered by another wallet;
  • detect v17 markets through configV17.marketauth;
  • recover a completed market whose authority rotated away through its LP portfolio;
  • exclude markets where the wallet owns only a trading portfolio;
  • preserve token-label resolution and truncated fallback labels;
  • propagate general discovery errors;
  • preserve v17 OI, insurance, liquidity, and attention-state helpers.

Validation

Targeted test suite

cd app

pnpm exec vitest run __tests__/components/MyMarkets.test.tsx \
  --reporter=verbose

Result:

Test Files  1 passed
Tests       29 passed

TypeScript

pnpm --filter @percolator/app exec tsc --noEmit

Result:

PASS

Production build

NEXT_PUBLIC_API_URL="http://127.0.0.1:3001" \
  pnpm --filter @percolator/app build

Result:

BUILD_EXIT_CODE=0

The application compiled successfully and completed static page generation.


Formatting and scope checks

git diff --check

Result:

PASS

Only the following files are changed:

app/hooks/useCreatedMarkets.ts
app/__tests__/components/MyMarkets.test.tsx

No package, lockfile, page component, Next.js configuration, or unrelated application source was modified.


Tooling Note

Direct ESLint execution is currently blocked by an existing repository dependency/configuration issue involving ESLint, @eslint/eslintrc, and AJV:

TypeError: Cannot set properties of undefined (setting 'defaultMeta')

The failure occurs while ESLint loads its configuration, before either changed file is analyzed.

This PR does not modify ESLint configuration, package dependencies, or the lockfile. Source validation is covered by the passing TypeScript check, targeted regression suite, and production build.


Files Changed

app/hooks/useCreatedMarkets.ts

  • adds wallet-scoped creator scan lifecycle state;
  • derives stable scan identities from wallet and market content;
  • participates in loading and error state;
  • invalidates stale wallet results;
  • prevents outdated async completions;
  • prevents duplicate scans for same-content discovery replacements;
  • refreshes both discovery and creator ownership state;
  • preserves authority and LP-portfolio creator detection.

app/__tests__/components/MyMarkets.test.tsx

  • adds four creator scan lifecycle regressions;
  • preserves all existing creator detection and display helper tests;
  • updates stale documentation to match the actual ownership scan behavior.

Scope and Security Clarification

This PR fixes client-side wallet-scoped data correctness and UI lifecycle handling.

It does not claim or modify:

  • on-chain authorization;
  • market ownership transfer;
  • transaction authorization;
  • administrative permissions;
  • private data access;
  • protocol fund movement.

On-chain authorization remains the final enforcement boundary.

The corrected behavior ensures that /my-markets no longer presents pending, failed, or previous-wallet ownership data as a final verified result.


Acceptance Criteria

  • Creator ownership scanning participates in loading state.
  • Empty state is not rendered while creator verification is pending.
  • Ownership RPC failures produce a recoverable error.
  • Failed verification is not treated as a successful empty scan.
  • Wallet A results are withheld when wallet B becomes active.
  • Stale asynchronous completions cannot update a newer wallet scan.
  • Creator results are exposed only for the active scan key.
  • Manual refresh invalidates and reruns creator ownership verification.
  • Same-content discovery replacements do not trigger duplicate RPC scans.
  • Rotated-authority creator detection through LP portfolios remains functional.
  • Trading-only portfolios remain excluded.
  • Existing MyMarkets tests remain green.
  • All four new lifecycle regressions pass.
  • TypeScript validation passes.
  • Production build succeeds.

@Bayyan16
Bayyan16 requested a review from dcccrypto as a code owner July 15, 2026 14:20
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

@Bayyan16 is attempting to deploy a commit to the Khubair Nasir's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c08945b-b6c1-465a-8df0-3a0cd9b97e35

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dcccrypto

Copy link
Copy Markdown
Owner

Independent verification — not an approval (QA/Security own that), just evidence for whoever reviews. Verdict: the tests bind to the fix.

Method: faithfully restored #2425 on the PR branch and re-ran the suite.

First attempt was wrong and I want to flag it, because the same trap caught me on #2430 and it's the main way this technique produces false accusations.

I initially neutralised only the render-time gate:

creatorScanKey !== null && creatorScanState.key === creatorScanKey
                        
creatorScanState.key !== null

All 29 tests passed — which reads as "the tests don't catch the bug". They do. This fix is layered, and I'd only disabled one layer:

  1. the render gate at :331, and
  2. the effect-side reset at :211-225, which on any key change immediately replaces state with {status:'loading', slabAddresses:[]}

With (2) still live the previous wallet's rows are cleared regardless, so green was correct. Your own comment there — "always has a new key and is withheld immediately by render-time gating" — is what tipped me off that there were two.

Neutralising both:

1 FAILED
  × withholds wallet A markets while wallet B ownership scan is pending

That's the exact reported symptom, and it's the only test that moves — the other 28 cover behaviour that legitimately shouldn't change.

Two notes

1. Tested at the component layer. MyMarkets.test.tsx drives the rendered component rather than the hook in isolation, so the guarantee is verified where a user experiences it and survives internal refactors. That's the standard worth holding — contrast #2437 / #2442 / #2434, where coverage sits below the layer the bug lives at. (I found the same gap in one of my own PRs, indexer#184, and fixed it — this isn't a standard I'm applying only outward.)

2. Defence in depth is good, but only one layer is pinned. The single failing test binds to the combination. If someone later deleted the render gate at :331 as redundant, all 29 would stay green — same as the effect-side reset being load-bearing on its own. Worth knowing before either is "simplified" later; not something I'd change now.

No changes requested from me.

@Bayyan16

Copy link
Copy Markdown
Contributor Author

Thank you for the careful layered mutation check and for correcting the initial partial mutation.

I understand that neutralising only the render-time gate did not restore the reported bug because the effect-side reset still cleared the previous wallet’s results. Neutralising both layers caused the exact wallet-switch regression to fail while the remaining tests stayed green, confirming that the component-level test binds to the user-visible invariant.

Since no changes were requested, I’ll keep the current PR head unchanged and leave formal review and merge decisions to the appropriate maintainers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants