Skip to content

feat: v4 tokenomics + count trees (YAPP token, buy/moderation UX)#291

Open
PastaPastaPasta wants to merge 11 commits into
masterfrom
feat/v4-tokenomics-count-trees
Open

feat: v4 tokenomics + count trees (YAPP token, buy/moderation UX)#291
PastaPastaPasta wants to merge 11 commits into
masterfrom
feat/v4-tokenomics-count-trees

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

Adopts Dash Platform v4 (protocol v12) for Yappr: count trees (O(1) proof-backed counts) and a native YAPP token with pay-per-action pricing, a minimum bulk buy-in bond (anti-spam), and freeze/slash moderation. Requires a cutover to a new v12 social contract.

New social contract (testnet): AyBNQV6qSLY8kZYHxbvhLrsDMSzWTKrWJBE6B7NYSxSh — count-tree indexes, dedicated repost doctype, YAPP token (owner-controlled), tokenCost per action. Contract validated end-to-end via the real DPP deserializer + a full audit of every app query against the indexes.

What's included

Counts → count trees

  • like / reply / follower / following / repost counts now use sdk.documents.count (O(1)) instead of O(n) pagination (also fixes the old 100-cap reply undercount).

Tokenomics

  • Token payment auto-attached on post/reply/like/repost (central in state-transition-service.createDocument), guarded by maximumTokenCost.
  • token-service.ts: balance, price, buyYapp (directPurchase), and authority freeze/unfreeze/destroyFrozen.
  • Pricing: min 100 YAPP per purchase @ 1,000,000 credits/token (tiered SetPrices, set via scripts/set-yapp-price.mjs since the JS facade is flat-only).

UX

  • Buy-YAPP modal (100-pack min, cost preview, credit-affordability check), YAPP balance + Buy button in the account dropdown, and insufficient-balance → Buy prompts across post/reply/like/repost.
  • Owner-gated Moderation settings section (freeze / unfreeze / slash).
  • Legacy-app escape link (LegacyYapprLink) on empty / end-of-feed states → sends users to the previous deployment for pre-cutover content.

Reposts

  • Migrated from "post with empty content" to a dedicated repost doctype (cheaper tokenCost, byPost count tree, postOwnerAndTime notification index).

Other

  • Bumps @dashevo/evo-sdk4.0.0-rc.2.
  • Removes dead primaryHashtag/firstMentionId post writes that the v2 schema's additionalProperties:false would reject.

Notes

  • Profiles need no migration — they live on a separate contract (YAPPR_PROFILE_CONTRACT_ID) untouched by this cutover.
  • The new contract starts empty; the feed shows a clean slate. The legacy link routes users to old content.
  • Moderation is a single owner key on testnet; mainnet should move it to a Group.

Testing

Build + lint pass. End-to-end manual testing on testnet is still pending (post→YAPP debit, counts, buy <100 rejected / ≥100 works, freeze blocks posting, slash burns). See the QA plan discussed in the working notes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an authority-gated Moderation section for freeze/unfreeze and destroy-frozen actions.
    • Added a multi-step “Buy YAPP” flow, plus a Buy balance item in the user menu.
    • Added legacy-app links for empty and end-of-feed states (including a “You’ve reached the end” message).
  • Bug Fixes
    • Improved insufficient-YAPP handling across posting, liking, and reposting by prompting to buy more when needed.
    • Tip announcements now warn if the public announcement note couldn’t be posted due to missing YAPP.
  • Performance
    • Faster like, repost, reply, and follower/following counts using index-based querying.

Migrate to a protocol-v12 social contract with count-tree indexes and a native YAPP token. Posts/replies/likes/reposts now cost YAPP (revenue to the contract owner), bought via a minimum-100 direct-purchase bond, with owner-controlled freeze/slash moderation.

Counts (likes/replies/followers/following/reposts) now use O(1) count trees instead of O(n) pagination. Token payment is auto-attached on all paid writes. Adds a Buy-YAPP modal, YAPP balance + Buy button, and insufficient-balance prompts across post/reply/like/repost. Reposts migrated to a dedicated 'repost' doctype. Bumps @dashevo/evo-sdk to 4.0.0-rc.2. Adds a legacy-app escape link on empty/end-of-feed states and scripts/set-yapp-price.mjs for tiered pricing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PastaPastaPasta, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a02d6b3e-e321-42a5-af3b-21cc1ed6ae68

📥 Commits

Reviewing files that changed from the base of the PR and between 1248873 and bf695e1.

📒 Files selected for processing (1)
  • components/token/buy-yapp-modal.tsx
📝 Walkthrough

Walkthrough

This PR adds YAPP token payment, purchase, and moderation flows; switches several counters to indexed counts; migrates reposts to dedicated documents; removes hashtag-based post fields and queries; and adds legacy-app links in empty and end-of-feed states.

Changes

YAPP Token Economy

Layer / File(s) Summary
Contract, constants, and SDK version
contracts/yappr-social-contract-v2.json, lib/constants.ts, package.json
Adds the v2 social/token contract schema, token constants, and Evo SDK dependency update.
TokenService and token-payment agreements
lib/services/token-service.ts, lib/services/state-transition-service.ts, scripts/set-yapp-price.mjs
Adds token balance and price lookup, buy and moderation actions, signer resolution, token-payment agreement construction, and the price-setting script.
Buy modal, balance item, and provider wiring
hooks/use-buy-yapp-modal.ts, components/token/buy-yapp-modal.tsx, components/token/yapp-balance-item.tsx, components/providers.tsx, components/layout/sidebar.tsx
Adds the buy-YAPP modal store and UI, the sidebar balance item, and wires both into the app shell.
Insufficient-token UX
lib/error-utils.ts, components/compose/compose-modal.tsx, components/post/post-card.tsx, components/post/tip-modal.tsx
Adds insufficient-token detection and routes compose, like, repost, and tip failures into the buy-YAPP flow.
Moderation settings and access gating
components/settings/moderation-settings.tsx, app/settings/page.tsx
Adds moderation token actions and exposes a moderation settings section only for the configured authority identity.

Indexed Counting and Repost Migration

Layer / File(s) Summary
Count helpers
lib/services/pagination-utils.ts, lib/services/sdk-helpers.ts
Adds shared helpers for O(1) document counts, grouped counts, and identifier decoding.
Follow, like, reply, and post stats counts
lib/services/follow-service.ts, lib/services/like-service.ts, lib/services/reply-service.ts, lib/services/post-stats-helpers.ts
Replaces paginated counting with indexed document counts and grouped counts for follow, like, reply, and batch post-stat reads.
Repost document migration
lib/services/repost-service.ts
Moves repost handling to dedicated repost documents, updates repost queries, and adds repost counts and per-user repost-id lookups.
Hashtag fields and queries removed
lib/services/post-service.ts, lib/dash-platform-client.ts, types/sdk.ts
Removes hashtag-related post document fields, post creation options, and hashtag query support.

Legacy App Link in Empty States

Layer / File(s) Summary
Legacy link component and empty action
lib/constants.ts, components/ui/legacy-yappr-link.tsx, components/ui/loading-state.tsx
Adds the legacy app URL, the external legacy link component, and an empty-state action prop.
Legacy link usage
app/hashtag/page.tsx, components/feed/feed-post-list.tsx
Renders the legacy link in the hashtag empty state and in feed loading and end-of-feed states.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BuyYappModal
  participant TokenService
  participant EvoSDK

  User->>BuyYappModal: enter amount and confirm
  BuyYappModal->>TokenService: buyYapp(buyerId, amount, maxTotalCost)
  TokenService->>EvoSDK: directPurchase(...)
  EvoSDK-->>TokenService: result or error
  TokenService-->>BuyYappModal: TokenResult
  BuyYappModal->>TokenService: getBalance(identityId)
  BuyYappModal->>User: success or error state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main v4 tokenomics and count-tree migration, plus the new YAPP buy and moderation UX.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v4-tokenomics-count-trees

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.

@thepastaclaw

thepastaclaw commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit bf695e1)

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying yappr with  Cloudflare Pages  Cloudflare Pages

Latest commit: bf695e1
Status: ✅  Deploy successful!
Preview URL: https://98498aad.yappr.pages.dev
Branch Preview URL: https://feat-v4-tokenomics-count-tre.yappr.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying yappr-v2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: bf695e1
Status: ✅  Deploy successful!
Preview URL: https://d5673650.yappr-v2.pages.dev
Branch Preview URL: https://feat-v4-tokenomics-count-tre.yappr-v2.pages.dev

View logs

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The token-service Map lookups almost certainly miss the SDK's Identifier-keyed results, which breaks the new YAPP balance/price/buy UX end-to-end. Beyond that, the feed-batch stats path still relies on capped document queries despite the PR advertising a count-tree cutover, and the new YAPP dropdown row traps the Buy button behind a disabled menu item. Everything else is minor cleanup.

Red: 1 blocking | Yellow: 2 suggestion(s) | Nit: 3 nitpick(s)

Findings not posted inline (3)

These findings could not be anchored to the current diff, but they are still part of this review.

  • [NITPICK] lib/services/token-service.ts:183-209: findMatchingAuthKey can reject a valid CRITICAL/HIGH key when a lower-security key matches firstfindMatchingKeyIndex scans the candidate list and returns the first WIF match, then this code post-filters that single result down to CRITICAL/HIGH. If an identity carries multiple AUTHENTICATION keys derived from the same WIF (for example, a MEDIUM key at a lower id and a CRITICAL key at a hig...
  • [NITPICK] lib/services/post-service.ts:20-21: Stale firstMentionId / primaryHashtag properties left on PostDocument — The PR intentionally drops writes and reads of firstMentionId / primaryHashtag because the v2 schema uses additionalProperties: false, but PostDocument here (and PostDocumentData in types/sdk.ts:170-171) still declares them. They're harmless (optional, unused) but they misrepresent th...
  • [NITPICK] components/post/post-card.tsx:393-400: Repost toast + optimistic count drift when a repost already existedrepostService.repostPost returns true early when an existing repost is found on-chain, but the caller has already applied reposts + 1 optimistically and shows the 'Reposted!' success toast on any truthy return. If the local reposted state was stale and the count fetched via the byPost cou...
Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `lib/services/token-service.ts`:
- [BLOCKING] lib/services/token-service.ts:30-62: YAPP balance and price lookups miss Identifier-keyed SDK Maps
  `sdk.tokens.identityBalances` and `sdk.tokens.directPurchasePrices` are declared in the evo-sdk facade as `Promise<Map<wasm.Identifier, bigint>>` and `Promise<Map<wasm.Identifier, TokenPriceInfo>>` — the Maps are keyed by wasm `Identifier` objects, not the base58 strings this code passes in. `Map.get(tokenId)` uses SameValueZero equality on those object keys, so `balances.get(tokenId)` and `prices.get(tokenId)` return `undefined` even when the SDK returned the requested entry. The account dropdown then shows 0 YAPP, `getPricePerToken()` returns `null`, and `buyYapp` errors out with 'YAPP is not currently for sale' — the new buy/balance UX never works. Iterate the Map entries and compare `key.toBase58()` (or equivalent) against `tokenId`, or convert `tokenId` to an `Identifier` before lookup.

In `lib/services/post-stats-helpers.ts`:
- [SUGGESTION] lib/services/post-stats-helpers.ts:140-183: Feed batch stats still use capped document fetches instead of count trees
  The PR migrates single-post `countLikes` / `countReposts` / `countReplies` to count trees (with a comment on the reply path noting 'also removes the old 100-cap undercount'), but `fetchBatchPostStats` — reached by feed cards via `postService.getBatchPostStats` in `useProgressiveEnrichment` — still delegates to `getLikesByPostIds`, `getRepostsByPostIds`, and `countRepliesByParentIds`, which each hit `sdk.documents.query` with a hard `limit: 100`. That means the same undercount the count-tree cutover eliminates for single-post views is preserved in the main feed once a batch of posts collectively has more than ~100 matching engagement documents. Since count-tree migration is a headline of this PR, the batch stats path should either issue per-post count-tree calls or a grouped `sdk.documents.count` query rather than continue truncating at 100.

In `components/token/yapp-balance-item.tsx`:
- [SUGGESTION] components/token/yapp-balance-item.tsx:31-45: Disabled DropdownMenu.Item hides the Buy button from keyboard users
  The Buy button is nested inside a `DropdownMenu.Item` marked `disabled` with `onSelect={(e) => e.preventDefault()}`. Radix removes disabled items from the roving focus, so keyboard-only users navigating the account dropdown cannot reach the Buy button at all — clicks work, but there is no way to open the buy modal via keyboard. Nesting an interactive `<button>` inside a `role="menuitem"` also violates ARIA's no-interactive-in-interactive rule. Either make the item itself the actionable target (`onSelect={() => openBuy()}` and drop the inner button) or lift the Buy control out of the DropdownMenu.Item so it participates in the menu's focus order.

Comment thread lib/services/token-service.ts Outdated
Comment thread components/token/yapp-balance-item.tsx Outdated
Comment thread lib/services/token-service.ts
- token-service: robust token-id Map lookup (SDK Maps may be Identifier-keyed) fixing balance/price/buy; pre-filter to CRITICAL/HIGH auth keys before matching so a lower-security key can't mask a valid one

- yapp-balance-item: make the Buy row a keyboard-operable DropdownMenu.Item instead of a button nested in a disabled item

- post-stats-helpers: batch feed stats now use per-post O(1) count trees (removes the 100-cap undercount on the feed path)

- remove stale firstMentionId/primaryHashtag from PostDocument/PostDocumentData types (v2 schema drops them)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@PastaPastaPasta

Copy link
Copy Markdown
Owner Author

Thanks for the review — addressed in 0652fc9:

  • 🔴 [BLOCKING] token-service Map lookups miss Identifier-keyed results — Fixed. Added a tokenMapGet helper that falls back from map.get(tokenId) to iterating entries and matching the string form of the key (base58()/String(key)), used by both getBalance and getPricePerToken. (Runtime testing had shown string keys, but this is robust regardless of whether the SDK returns string or Identifier keys.)
  • 🟡 [SUGGESTION] feed batch stats still capped at 100 — Fixed. fetchBatchPostStats now issues per-post O(1) count-tree reads (countLikes/countReposts/countReplies) instead of the capped in-queries, so the feed path no longer undercounts once a page collectively exceeds ~100 engagements.
  • 🟡 [SUGGESTION] disabled DropdownMenu.Item traps the Buy button from keyboard — Fixed. The whole row is now the actionable DropdownMenu.Item (onSelect={openBuy}, no longer disabled); the "Buy" pill is a non-interactive <span>, so it's keyboard-operable and no longer nests an interactive control in a role="menuitem".
  • 💬 [NITPICK] findMatchingAuthKey can reject a valid CRITICAL/HIGH key — Fixed. Now filters authKeys to CRITICAL/HIGH before findMatchingKeyIndex, so a lower-security key derived from the same WIF can't win the match and mask a valid one.
  • 💬 [NITPICK] stale firstMentionId/primaryHashtag on PostDocument/PostDocumentData — Fixed. Removed both declarations (no readers remained).
  • 💬 [NITPICK] repost toast/optimistic count drift when a repost already existed — Acknowledged, left as-is. It only occurs on stale local state (already reposted on-chain), mirrors the existing optimistic pattern used for likes, and self-corrects on the next progressive enrichment via the count tree. Happy to tighten it if you'd prefer repostPost to distinguish created-vs-already-existed.

Build + lint pass.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

All six prior findings from the 6c6632d review are addressed: five are fixed in code (tokenMapGet handles Identifier-keyed Maps; fetchBatchPostStats uses per-post count trees; the YAPP row is a keyboard-actionable DropdownMenu.Item; findMatchingAuthKey pre-filters CRITICAL/HIGH; stale firstMentionId/primaryHashtag properties removed from source), and the repost toast drift nit is intentionally deferred with a reasonable rationale. Two new concerns emerge from the current buy path: buyYapp uses a freshly-refetched chain price as the maxTotalCost cap rather than the price the user confirmed, so a mid-flight SetPrices bump can silently authorize a higher spend; and purchase amounts are round-tripped through JS number, which the review skill explicitly warns against for token/credit values.

🔴 1 blocking | 🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `lib/services/token-service.ts`:
- [BLOCKING] lib/services/token-service.ts:96-125: maxTotalCost is set from a fresh chain price, not the price the user confirmed
  The buy modal shows the user a cost derived from `pricePerToken` loaded once when the modal opens (`components/token/buy-yapp-modal.tsx:40,54`). When the user then clicks Confirm & Buy, `buyYapp()` deliberately refetches the current direct-purchase price and uses `price * amount` as `maxTotalCost` (lines 103-107), with a comment framing this as a safety property. In practice this inverts the guard: if the token authority raises the YAPP SetPrices tier between the modal render and the user's confirmation, the state transition is signed authorizing the *new* higher spend without the user ever seeing that number, and there is no on-chain rejection because `maxTotalCost` already accommodates it. Per the review skill's token/payment rule, `maxTotalCost` should represent the user-approved spend: pass the quoted price (or quoted total) into `buyYapp()` and use that as the cap, so a mid-flight price increase surfaces as an error the user can re-quote instead of silently going through at the higher price.

In `components/token/buy-yapp-modal.tsx`:
- [SUGGESTION] components/token/buy-yapp-modal.tsx:53-82: YAPP purchase amount and cost are coerced through JS number before signing
  `amountNum = parseInt(amount, 10)` (line 53) parses user input into a JS `number`, and that value is later converted back to `BigInt` when calling `tokenService.buyYapp(user.identityId, BigInt(amountNum))` (line 82). `costCredits` is similarly funneled through `Number(costCredits)` for display (line 56). While realistic YAPP purchase amounts stay well below `Number.MAX_SAFE_INTEGER`, the review skill explicitly calls out this class of round-trip: 'Do not coerce token/credit values through `Number` when precision can matter; prefer string/BigInt-safe handling.' For token/credit paths, keep the input parsed directly to `BigInt` (rejecting non-numeric strings) and format credits via BigInt-safe helpers so the signed amount can never diverge from what the UI showed.

Comment thread lib/services/token-service.ts Outdated
Comment thread components/token/buy-yapp-modal.tsx Outdated
- buyYapp now takes the user-approved maxTotalCost (the quoted total the modal showed) instead of refetching a fresh price at sign time, so a mid-flight YAPP price increase is rejected on-chain rather than silently overspending; classify the price-too-low rejection as a re-quote prompt

- buy modal keeps amount and cost as BigInt end-to-end (no parseInt/Number round-trip through the signed path) and formats credits->DASH with a BigInt-safe helper

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@PastaPastaPasta

Copy link
Copy Markdown
Owner Author

Round 2 addressed in 911ee70:

  • 🔴 [BLOCKING] maxTotalCost set from a fresh chain price, not the confirmed price — Fixed. buyYapp(buyerId, amount, maxTotalCost) now takes the exact total the user approved in the modal (the quoted costCredits) as the cap and no longer refetches the price at sign time. If the SetPrices tier rose between quote and confirm, the transition is rejected on-chain and surfaced as "The YAPP price changed — please reopen and confirm the new cost," so the higher spend can never go through silently.
  • 🟡 [SUGGESTION] amount/cost coerced through JS number before signing — Fixed. The modal keeps the amount and cost as BigInt end-to-end (amountBig = BigInt(amount), costCredits = pricePerToken * amountBig), passes both straight to buyYapp, and formats credits→DASH with a BigInt-safe helper. No parseInt/Number round-trip touches the signed values.

Build + lint pass. (Prior round's three findings were auto-resolved by the review system.)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Both prior findings from 0652fc9 are FIXED at head 911ee70: buyYapp now signs with a caller-supplied maxTotalCost matching the exact quote the user confirmed in the modal, and the purchase amount + cost stay BigInt end-to-end via a strict /^\d+$/ regex and the new BigInt-safe formatCreditsAsDash helper. No new blocking or suggestion-level defects found in the latest delta; the only new finding is a docs nit that a leftover comment on getPricePerToken misattributes the maxTotalCost-staleness protection to that function even though the fix moved the guarantee to the caller-supplied cap.

💬 1 nitpick(s)

Comment thread lib/services/token-service.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/settings/page.tsx (1)

750-754: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Header title doesn't account for the moderation section.

getSectionTitle only searches settingsSections, which never includes MODERATION_SECTION (it's spread in locally at Line 215 inside renderMainSettings). Visiting the moderation section falls back to "Settings" instead of "Moderation".

🏷️ Proposed fix
   const getSectionTitle = () => {
     if (activeSection === 'main') return 'Settings'
-    const section = settingsSections.find(s => s.id === activeSection)
+    const section = [...settingsSections, MODERATION_SECTION].find(s => s.id === activeSection)
     return section?.label || 'Settings'
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/settings/page.tsx` around lines 750 - 754, The header title logic in
getSectionTitle does not handle the moderation section because it only looks up
settingsSections, so activeSection values for MODERATION_SECTION fall back to
“Settings.” Update getSectionTitle to also recognize the moderation section by
checking the same moderation identifier used in renderMainSettings and returning
the proper “Moderation” label before falling back to the existing
settingsSections lookup.
lib/services/reply-service.ts (1)

354-366: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Error swallowed silently — no logging, unlike sibling count methods.

countFollowers/countFollowing (follow-service.ts), countLikes (like-service.ts), and countReposts (repost-service.ts) all logger.error(...) before returning 0 on failure. This catch {} block returns 0 with no log, making failures here (e.g., missing index, bad query) invisible in production.

🪵 Add logging for consistency
   async countReplies(parentId: string): Promise<number> {
     try {
       const sdk = await getEvoSdk();
       // O(1) count tree on the `byParent` index [parentId] (also removes the old 100-cap undercount).
       return await documentCount(sdk, {
         dataContractId: this.contractId,
         documentTypeName: 'reply',
         where: [['parentId', '==', parentId]],
       });
-    } catch {
+    } catch (error) {
+      logger.error('Error counting replies:', error);
       return 0;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/reply-service.ts` around lines 354 - 366, The `countReplies`
method in `reply-service.ts` is swallowing errors silently, unlike the sibling
count methods. Update the `catch` block in `countReplies` to log the failure
with `logger.error(...)` before returning 0, using the same pattern as
`countFollowers`, `countFollowing`, `countLikes`, and `countReposts` so failures
in `getEvoSdk` or `documentCount` are visible.
🧹 Nitpick comments (10)
components/settings/moderation-settings.tsx (2)

22-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reset the form after a successful action.

targetId/note remain populated after freeze/unfreeze/destroyFrozen succeeds, risking accidental resubmission against the same identity.

🧹 Proposed fix to clear inputs after success
     if (result.success) {
       toast.success(
         action === 'freeze' ? 'Identity frozen (suspended)'
           : action === 'unfreeze' ? 'Identity unfrozen (reinstated)'
           : 'Frozen balance destroyed (slashed)'
       )
+      setTargetId('')
+      setNote('')
     } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/settings/moderation-settings.tsx` around lines 22 - 49, The
moderation action flow in run() leaves targetId and note filled after a
successful tokenService call, so clear both inputs only when result.success is
true. Update the ModerationSettings component logic around run(), using the
existing setBusy/result handling, to reset the form state after the success
toast before returning.

63-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared Input component here
Replace the two raw <input> elements with components/ui/input.tsx so both moderation fields use the app’s standard styling and focus behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/settings/moderation-settings.tsx` around lines 63 - 69, The
moderation form still uses raw HTML inputs instead of the shared standard field
component. Update the two input fields in ModerationSettings to use the shared
Input from components/ui/input.tsx, keeping the existing
value/onChange/placeholder behavior but moving styling and focus handling to the
reusable component so both moderation fields match the app’s UI conventions.

Source: Coding guidelines

package.json (1)

16-16: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Pin the release-candidate SDK while token APIs are still settling.

This PR depends on new evo-sdk token/direct-purchase APIs; using ^4.0.0-rc.2 can pull a newer RC/final API during lock refreshes. Consider pinning exactly until the v4 API is stable.

Proposed dependency pin
-    "`@dashevo/evo-sdk`": "^4.0.0-rc.2",
+    "`@dashevo/evo-sdk`": "4.0.0-rc.2",

Please also verify with the project build before committing. As per coding guidelines, “Run build with npm run build to ensure TypeScript compilation succeeds and static export works correctly before committing.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 16, The dependency range for `@dashevo/evo-sdk` is too
loose and can float to newer RC/final API changes during lockfile refreshes. Pin
the version exactly in package.json where the evo-sdk dependency is declared,
and keep the token/direct-purchase code aligned with that fixed SDK version.
After updating the dependency, run the project build through npm run build to
confirm TypeScript compilation and static export still succeed before
committing.

Source: Coding guidelines

components/post/post-card.tsx (1)

369-373: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider consolidating the repeated insufficient-token branch.

The isInsufficientTokenError(error) ? open buy modal : toast.error(...) pattern is now duplicated here (twice) and once more in components/compose/compose-modal.tsx. A small shared helper (e.g. handleTokenAwareError(error, reason, fallbackMessage) in lib/error-utils.ts or a useTokenAwareErrorHandler hook) would remove the triplication and reduce the chance of a future call site forgetting the insufficient-token check.

♻️ Example consolidated helper
// lib/error-utils.ts
export function handleTokenAwareError(error: unknown, reason: string, fallbackMessage: string) {
  if (isInsufficientTokenError(error)) {
    useBuyYappModal.getState().open(reason)
  } else {
    toast.error(fallbackMessage)
  }
}
-      if (isInsufficientTokenError(error)) {
-        useBuyYappModal.getState().open('You need YAPP to like posts. Buy some to continue.')
-      } else {
-        toast.error('Failed to update like. Please try again.')
-      }
+      handleTokenAwareError(error, 'You need YAPP to like posts. Buy some to continue.', 'Failed to update like. Please try again.')

Also applies to: 406-410

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/post/post-card.tsx` around lines 369 - 373, The insufficient-token
handling is duplicated in PostCard and again in the compose flow, so extract the
repeated `isInsufficientTokenError(error)` branch into a shared helper or hook
such as `handleTokenAwareError` or `useTokenAwareErrorHandler`. Update the
current `PostCard` error path and the matching call site in `ComposeModal` to
use that shared utility, passing the modal reason and fallback toast message so
`useBuyYappModal` and `toast.error` behavior stays consistent in one place.
scripts/set-yapp-price.mjs (4)

32-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded contract/token constants duplicate lib/constants.ts.

CONTRACT_ID and TOKEN_POS mirror YAPPR_CONTRACT_ID / YAPP_TOKEN_POSITION referenced in token-service.ts (per the upstream contract snippet). Duplicating them here risks drift if the contract is redeployed (a common occurrence pre-mainnet) and only this script is updated, or vice versa.

♻️ Reuse shared constants
-const CONTRACT_ID = 'AyBNQV6qSLY8kZYHxbvhLrsDMSzWTKrWJBE6B7NYSxSh';
-const TOKEN_POS = 0;
+import { YAPPR_CONTRACT_ID, YAPP_TOKEN_POSITION } from '../lib/constants.js';
+const CONTRACT_ID = YAPPR_CONTRACT_ID;
+const TOKEN_POS = YAPP_TOKEN_POSITION;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/set-yapp-price.mjs` around lines 32 - 37, The hardcoded contract and
token identifiers in the price-setting script duplicate shared values and can
drift from the rest of the codebase. Update the script to reuse the existing
constants from the shared constants module instead of defining its own
CONTRACT_ID and TOKEN_POS, keeping set-yapp-price.mjs aligned with
token-service.ts and the contract config.

110-125: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Fragile error detection via lowercase substring match.

Distinguishing the "unverifiable proof for a just-registered contract" case from real broadcast failures relies on msg.includes('unknown contract'). If the SDK's error wording changes in a future evo-sdk release, this script will misclassify a genuine failure as the benign case and report false success (it proceeds to read back prices without confirming the transition actually applied via a return-code check).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/set-yapp-price.mjs` around lines 110 - 125, The try/catch around
sdk.stateTransitions.broadcastStateTransition and
sdk.stateTransitions.waitForResponse is using a fragile lowercase substring
match on the error message to detect the benign “just-registered contract” case.
Replace the text-based check with a more stable signal from the evo-sdk error
object or a dedicated error code/type if available, and only fall back to
message inspection as a last resort. Keep the existing confirm-via-price-read
path in the benign case, but ensure real broadcast failures still hit the
BROADCAST ERROR path and exit.

37-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded personal file path reduces portability.

IDENTITY_FILE points to a fixed ~/Downloads/... path tied to one developer's machine. Anyone else running this one-shot script needs to know to place the identity JSON there, or edit the source. Consider an env var or CLI arg (e.g., process.argv[2] ?? join(homedir(), 'Downloads', ...)).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/set-yapp-price.mjs` at line 37, The hardcoded IDENTITY_FILE path in
set-yapp-price.mjs ties the script to one machine’s Downloads folder and hurts
portability. Update the script to accept the identity JSON location from an
environment variable or CLI argument first, and keep the current joined homedir
fallback only as a default. Use the existing IDENTITY_FILE constant and the
script’s argument parsing/startup logic to wire this in without requiring source
edits for other users.

36-36: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No runtime check that the resolved key actually has CRITICAL security level.

The comment on Line 36 states token config transitions require a CRITICAL auth key, and securityLevel is logged (Line 54) but never validated before signing. If key id 2 on a given identity happens to be a lower security level, this fails only at broadcast/validation time with a less clear on-chain error, rather than failing fast with an actionable message. This mirrors the review-fix in token-service.ts where auth keys are filtered to CRITICAL/HIGH before matching.

🔒 Fail fast on wrong security level
 const signingKey = (identityJson.identityKeys || []).find((k) => k.id === SIGNING_KEY_ID);
 if (!signingKey?.privateKeyWif) throw new Error(`No privateKeyWif for key id ${SIGNING_KEY_ID}`);
+if (signingKey.securityLevel !== 'CRITICAL') {
+  throw new Error(`Key id ${SIGNING_KEY_ID} is ${signingKey.securityLevel}, expected CRITICAL`);
+}

Also applies to: 52-54, 99-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/set-yapp-price.mjs` at line 36, The signing flow in
set-yapp-price.mjs assumes SIGNING_KEY_ID 2 is CRITICAL, but the resolved key’s
securityLevel is only logged and never enforced. Update the key lookup/signing
path around the key resolution and logging logic to validate that the selected
auth key is actually CRITICAL before any signing happens, and fail fast with a
clear error if it is not. Mirror the token-service.ts approach by filtering or
checking for CRITICAL (and only proceeding when the matched key meets that
level) in the code that resolves the signer and prepares the transaction.
lib/services/post-stats-helpers.ts (1)

158-173: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Per-post fan-out: 3×N individual count-tree calls per batch.

fetchBatchPostStats now issues postIds.length * 3 separate documentCount round trips (likes/reposts/replies each queried per post). This fixes the prior 100-item undercount, but for larger feed pages this is a meaningful increase in concurrent DAPI requests compared to the old batched in-queries.

The documentCount doc comment itself notes grouped counts are supported (groupBy: ['postId'] with postId in [...], reading per-key entries from sdk.documents.count directly). Worth exploring a grouped-count helper here to collapse each stat type into a single request across the batch, if groupBy semantics on the count-tree behave as documented.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/post-stats-helpers.ts` around lines 158 - 173,
`fetchBatchPostStats` in `post-stats-helpers.ts` is doing 3×N per-post count
calls via `likeService.countLikes`, `repostService.countReposts`, and
`replyService.countReplies`; refactor this batch path to use a grouped-count
helper with `groupBy: ['postId']` and `postId in [...]` so each stat type is
fetched in one request per batch. Keep the existing per-post assignment logic
that fills `result` from the returned counts, but update the lookup to read
grouped counts from the new helper instead of calling the count services inside
`Promise.all`.
lib/services/repost-service.ts (1)

184-206: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Same 'in'+limit:100 undercount risk as getLikesByPostIds, but no caveat comment here.

like-service.ts's getLikesByPostIds carries an explicit TODO noting 'in' queries don't paginate reliably and can silently truncate once the batch exceeds the limit. getRepostsByPostIds has the identical query shape (postId in postIds, limit: 100) but no equivalent note, so a future reader could miss this known SDK limitation when batches of heavily-reposted posts are queried together.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/repost-service.ts` around lines 184 - 206, `getRepostsByPostIds`
has the same `where: [['postId', 'in', postIds]]` plus `limit: 100` truncation
risk as `getLikesByPostIds`, but it lacks the warning note. Add the same
caveat/TODO comment near `RepostService.getRepostsByPostIds` so readers know the
SDK’s `'in'` query may silently undercount when the batch exceeds the limit,
matching the guidance already present in `like-service.ts`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/settings/moderation-settings.tsx`:
- Around line 61-70: The form fields in moderation-settings.tsx have unlabeled
inputs because the <label> elements are not associated with their corresponding
<input> elements. Update the input groups in this component so each label/input
pair is linked with matching htmlFor and id values, using the existing fields
like targetId and the other related inputs in this section. Ensure each label
text remains tied to its specific input for accessibility and screen readers.

In `@lib/services/token-service.ts`:
- Around line 238-239: Fix the under-minimum matcher typo in token-service’s
minimum purchase check. In the logic that normalizes the error string and
matches against `undeminimum`, `under minimum`, and `minimum sale`, update the
compact variant so it correctly matches `underminimum` (or equivalent normalized
form) and still returns the `BELOW_MINIMUM` response from this branch instead of
falling through to `NETWORK_ERROR`.

In `@scripts/set-yapp-price.mjs`:
- Line 84: The SetPrices call in the price-setting script is converting the
price to a Number even though TokenPricingSchedule.SetPrices expects bigint
values. Update the schedule construction to pass CREDITS_PER_TOKEN directly in
the SetPrices mapping, and keep the MIN_PURCHASE key conversion as-is so the
price stays precise in set-yapp-price.mjs.

---

Outside diff comments:
In `@app/settings/page.tsx`:
- Around line 750-754: The header title logic in getSectionTitle does not handle
the moderation section because it only looks up settingsSections, so
activeSection values for MODERATION_SECTION fall back to “Settings.” Update
getSectionTitle to also recognize the moderation section by checking the same
moderation identifier used in renderMainSettings and returning the proper
“Moderation” label before falling back to the existing settingsSections lookup.

In `@lib/services/reply-service.ts`:
- Around line 354-366: The `countReplies` method in `reply-service.ts` is
swallowing errors silently, unlike the sibling count methods. Update the `catch`
block in `countReplies` to log the failure with `logger.error(...)` before
returning 0, using the same pattern as `countFollowers`, `countFollowing`,
`countLikes`, and `countReposts` so failures in `getEvoSdk` or `documentCount`
are visible.

---

Nitpick comments:
In `@components/post/post-card.tsx`:
- Around line 369-373: The insufficient-token handling is duplicated in PostCard
and again in the compose flow, so extract the repeated
`isInsufficientTokenError(error)` branch into a shared helper or hook such as
`handleTokenAwareError` or `useTokenAwareErrorHandler`. Update the current
`PostCard` error path and the matching call site in `ComposeModal` to use that
shared utility, passing the modal reason and fallback toast message so
`useBuyYappModal` and `toast.error` behavior stays consistent in one place.

In `@components/settings/moderation-settings.tsx`:
- Around line 22-49: The moderation action flow in run() leaves targetId and
note filled after a successful tokenService call, so clear both inputs only when
result.success is true. Update the ModerationSettings component logic around
run(), using the existing setBusy/result handling, to reset the form state after
the success toast before returning.
- Around line 63-69: The moderation form still uses raw HTML inputs instead of
the shared standard field component. Update the two input fields in
ModerationSettings to use the shared Input from components/ui/input.tsx, keeping
the existing value/onChange/placeholder behavior but moving styling and focus
handling to the reusable component so both moderation fields match the app’s UI
conventions.

In `@lib/services/post-stats-helpers.ts`:
- Around line 158-173: `fetchBatchPostStats` in `post-stats-helpers.ts` is doing
3×N per-post count calls via `likeService.countLikes`,
`repostService.countReposts`, and `replyService.countReplies`; refactor this
batch path to use a grouped-count helper with `groupBy: ['postId']` and `postId
in [...]` so each stat type is fetched in one request per batch. Keep the
existing per-post assignment logic that fills `result` from the returned counts,
but update the lookup to read grouped counts from the new helper instead of
calling the count services inside `Promise.all`.

In `@lib/services/repost-service.ts`:
- Around line 184-206: `getRepostsByPostIds` has the same `where: [['postId',
'in', postIds]]` plus `limit: 100` truncation risk as `getLikesByPostIds`, but
it lacks the warning note. Add the same caveat/TODO comment near
`RepostService.getRepostsByPostIds` so readers know the SDK’s `'in'` query may
silently undercount when the batch exceeds the limit, matching the guidance
already present in `like-service.ts`.

In `@package.json`:
- Line 16: The dependency range for `@dashevo/evo-sdk` is too loose and can float
to newer RC/final API changes during lockfile refreshes. Pin the version exactly
in package.json where the evo-sdk dependency is declared, and keep the
token/direct-purchase code aligned with that fixed SDK version. After updating
the dependency, run the project build through npm run build to confirm
TypeScript compilation and static export still succeed before committing.

In `@scripts/set-yapp-price.mjs`:
- Around line 32-37: The hardcoded contract and token identifiers in the
price-setting script duplicate shared values and can drift from the rest of the
codebase. Update the script to reuse the existing constants from the shared
constants module instead of defining its own CONTRACT_ID and TOKEN_POS, keeping
set-yapp-price.mjs aligned with token-service.ts and the contract config.
- Around line 110-125: The try/catch around
sdk.stateTransitions.broadcastStateTransition and
sdk.stateTransitions.waitForResponse is using a fragile lowercase substring
match on the error message to detect the benign “just-registered contract” case.
Replace the text-based check with a more stable signal from the evo-sdk error
object or a dedicated error code/type if available, and only fall back to
message inspection as a last resort. Keep the existing confirm-via-price-read
path in the benign case, but ensure real broadcast failures still hit the
BROADCAST ERROR path and exit.
- Line 37: The hardcoded IDENTITY_FILE path in set-yapp-price.mjs ties the
script to one machine’s Downloads folder and hurts portability. Update the
script to accept the identity JSON location from an environment variable or CLI
argument first, and keep the current joined homedir fallback only as a default.
Use the existing IDENTITY_FILE constant and the script’s argument
parsing/startup logic to wire this in without requiring source edits for other
users.
- Line 36: The signing flow in set-yapp-price.mjs assumes SIGNING_KEY_ID 2 is
CRITICAL, but the resolved key’s securityLevel is only logged and never
enforced. Update the key lookup/signing path around the key resolution and
logging logic to validate that the selected auth key is actually CRITICAL before
any signing happens, and fail fast with a clear error if it is not. Mirror the
token-service.ts approach by filtering or checking for CRITICAL (and only
proceeding when the matched key meets that level) in the code that resolves the
signer and prepares the transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5161e7e0-ff6a-4b84-b95b-880820e79427

📥 Commits

Reviewing files that changed from the base of the PR and between 3d0fb1e and 911ee70.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • app/hashtag/page.tsx
  • app/settings/page.tsx
  • components/compose/compose-modal.tsx
  • components/feed/feed-post-list.tsx
  • components/layout/sidebar.tsx
  • components/post/post-card.tsx
  • components/providers.tsx
  • components/settings/moderation-settings.tsx
  • components/token/buy-yapp-modal.tsx
  • components/token/yapp-balance-item.tsx
  • components/ui/legacy-yappr-link.tsx
  • components/ui/loading-state.tsx
  • contracts/yappr-social-contract-v2.json
  • hooks/use-buy-yapp-modal.ts
  • lib/constants.ts
  • lib/dash-platform-client.ts
  • lib/error-utils.ts
  • lib/services/follow-service.ts
  • lib/services/like-service.ts
  • lib/services/pagination-utils.ts
  • lib/services/post-service.ts
  • lib/services/post-stats-helpers.ts
  • lib/services/reply-service.ts
  • lib/services/repost-service.ts
  • lib/services/state-transition-service.ts
  • lib/services/token-service.ts
  • package.json
  • scripts/set-yapp-price.mjs
  • types/sdk.ts
💤 Files with no reviewable changes (3)
  • types/sdk.ts
  • lib/dash-platform-client.ts
  • lib/services/post-service.ts

Comment thread components/settings/moderation-settings.tsx
Comment thread lib/services/token-service.ts Outdated
});
console.log('built TokenBaseTransition');

const schedule = TokenPricingSchedule.SetPrices({ [MIN_PURCHASE.toString()]: Number(CREDITS_PER_TOKEN) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@dashevo/evo-sdk TokenPricingSchedule.SetPrices value type BigInt or Number

💡 Result:

In the @dashevo/evo-sdk, the values used within the TokenPricingSchedule.SetPrices structure are expected to be of type BigInt [1]. The underlying protocol implementation (wasm-dpp) represents these prices as credit amounts, which are inherently 64-bit integers [1]. While some internal components or lenient parsing paths might accept other types like Number or String for convenience during JS-level input, relying on these can lead to truncation, precision loss, or runtime errors [1]. Specifically, passing a JavaScript Number greater than 2^53-1 will cause precision issues, as Number types cannot safely represent the full range of 64-bit integers [1]. To ensure compatibility with the protocol's expected BigInt values and to avoid issues with silent truncation or type-checking failures, you should use BigInt for all credit price values in your price tiers [1]. If you are working with TypeScript, the SDK expects these values to be mapped as a Record<string, bigint> [1].

Citations:


Pass the schedule price as bigint
TokenPricingSchedule.SetPrices expects bigint values here, so Number(CREDITS_PER_TOKEN) is unnecessary and can lose precision if this value grows. Use CREDITS_PER_TOKEN directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/set-yapp-price.mjs` at line 84, The SetPrices call in the
price-setting script is converting the price to a Number even though
TokenPricingSchedule.SetPrices expects bigint values. Update the schedule
construction to pass CREDITS_PER_TOKEN directly in the SetPrices mapping, and
keep the MIN_PURCHASE key conversion as-is so the price stays precise in
set-yapp-price.mjs.

…typo)

- settings header title now includes the moderation section (was falling back to 'Settings')

- fix 'underminimum' matcher typo so compact under-minimum errors classify correctly

- associate moderation form labels with inputs via htmlFor/id

- set-yapp-price: keep Number for SetPrices (the wasm binding rejects BigInt) but guard against the >2^53 precision cliff

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@PastaPastaPasta

Copy link
Copy Markdown
Owner Author

Thanks @coderabbitai — addressed in 29db286:

  • settings header title omits the moderation section — Fixed. getSectionTitle now searches [...settingsSections, MODERATION_SECTION], so the header reads "Moderation".
  • underminimum matcher typo — Fixed (undeminimumunderminimum).
  • moderation form labels not associated with inputs — Fixed. Added htmlFor/id pairs on both fields.
  • SetPrices value should be BigInt — Kept as Number, intentionally. At runtime this SDK's wasm binding rejects a BigInt here ("Price for amount '100' must be an integer") and only accepts a JS number — verified when actually setting the price on-chain. I added a guard that throws if the price ever exceeds Number.MAX_SAFE_INTEGER, so the 2^53 precision concern is covered while staying compatible with the binding.

Build + lint pass.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@PastaPastaPasta Thanks for the quick turnaround — all four points look good:

  • Moderation section now included in getSectionTitle lookup — correct fix.
  • underminimum typo fix confirmed.
  • Label/input association via htmlFor/id — resolves the a11y concern.
  • SetPrices as Number — makes sense given the wasm binding's runtime rejection of BigInt there; the Number.MAX_SAFE_INTEGER guard is a reasonable mitigation for the precision concern.

Nice work getting this across the finish line through two review rounds.

(ᵔᴥᵔ) glad this one's wrapped up


✏️ Learnings added
Learnt from: PastaPastaPasta
URL: https://github.com/PastaPastaPasta/yappr/pull/291

Timestamp: 2026-07-01T21:27:28.131Z
Learning: In the yappr repository (scripts/set-yapp-price.mjs), when constructing a `TokenPricingSchedule.SetPrices` transition via the `dashevo/evo-sdk` wasm bindings (v4.0.0-rc.2), the price value must be passed as a JS `Number`, not a `BigInt` — the binding throws "Price for amount 'X' must be an integer" if given a BigInt. A guard should be added to throw if the price value exceeds `Number.MAX_SAFE_INTEGER` to avoid silent precision loss.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Carried-forward: prior-1 (stale JSDoc on getPricePerToken at lib/services/token-service.ts:65-68) is STILL VALID at head 29db286 — the comment still misattributes the maxTotalCost staleness protection to getPricePerToken, while the actual guarantee is enforced by buyYapp's caller-supplied cap (lines 92-98). New in latest delta: no new defects — the four small changes (settings section-title lookup, moderation-settings a11y htmlFor/id pairing, 'undeminimum'→'underminimum' matcher typo fix, and safe-integer guard in scripts/set-yapp-price.mjs) are all correct in-scope fixes to the CodeRabbit round-2 nits. All three agents (codex, opus, sonnet5) converge on the same single carried-forward nit.

💬 1 nitpick(s)

Carried-forward findings already raised (1)

These findings were not re-posted as new inline comments because an existing review thread already covers them.

  • [NITPICK] (deduped existing open thread) lib/services/token-service.ts:65-68: Stale doc comment on getPricePerToken after the maxTotalCost fix — The JSDoc on getPricePerToken still reads: "Fetched fresh each call so maxTotalCost never uses a stale price." That was accurate when buyYapp called getPricePerToken() internally right before signing, but the safety property has since moved: buyYapp(buyerId, amount, maxTotalCost) at lin...

PastaPastaPasta and others added 2 commits July 1, 2026 17:17
Correctness: surface tip announcement-reply failures for 0-YAPP tippers (#7); user like/repost state now queries the user's own docs via composite indexes instead of capped all-user queries (#8); getBalance no longer swallows failures to 0 (#11); broaden insufficient-credits error classification and stop misclassifying network/proof errors as bad-key (#9,#10); gate the settings Moderation title by isAuthority (#12); bound the per-post count fan-out concurrency (#1); log missing count-tree totals (#13); log invalid repost postIds (#4).

Cleanup: extract shared handleInsufficientYapp helper (was copy-pasted 3x, #5); remove dead quoteCost (#6) and dead reply-batch methods (#14).

Deferred: cross-service key-matching dedup (#2) — a broad refactor of pre-existing signing code, out of scope. Grouped-count optimization for #1 deferred until the hex group-key layout can be verified against live data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fetchBatchPostStats fired 3xN individual count-tree calls per feed page (one per like/repost/reply per post), previously only bounded to 6-concurrent rather than fixed. Replace with a single grouped sdk.documents.count({ groupBy }) call per stat type, decoding the SDK's hex-keyed grouped response back to base58 postIds via the new identifierToHex helper.

Falls back to the proven per-post reads if the grouped response doesn't decode against expected keys (unverified against live testnet data), so a wrong hex-encoding assumption degrades to the old behavior instead of silently reporting zero counts.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/services/tip-service.ts (1)

332-351: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Generic error swallowing loses failure-cause detail consumed by the UI.

createTipPost catches all errors uniformly and returns false, so callers (e.g., tip-modal.tsx) cannot distinguish "insufficient YAPP" from other failures (network, validation). The UI currently hardcodes a YAPP-specific message for any false result — see companion comment in components/post/tip-modal.tsx (lines 208-214).

If precise user messaging matters, consider returning/propagating a reason (e.g., an error code or message) alongside the boolean instead of only logging it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/tip-service.ts` around lines 332 - 351, createTipPost in
tip-service.ts is swallowing all failures by logging and returning only false,
which prevents callers like tip-modal.tsx from telling insufficient YAPP apart
from network or validation errors. Update createTipPost to propagate a reason
along with the success state, such as an error code/message or a typed result,
and preserve the original error details from the catch block instead of
collapsing everything to a generic boolean. Keep the existing flow in
replyService.createReply and the logger.error path, but make the return value
from createTipPost specific enough for the UI to choose the correct user-facing
message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/post/tip-modal.tsx`:
- Around line 208-214: The toast message in `tip-modal` is assuming the public
tip note failed because of YAPP, but `createTipPost` in `tip-service` returns
`announcementPosted === false` for any error. Update the messaging in the tip
flow to avoid naming a specific cause unless it is explicitly known, and adjust
`createTipPost` to surface a distinguishable failure reason if you want precise
user-facing wording. Use the `announcementPosted` check in `TipModal` and the
generic catch in `createTipPost` as the main points to revise.

In `@lib/services/like-service.ts`:
- Around line 185-195: getUserLikedPostIds in like-service.ts is limited by the
fixed sdk.documents.query limit of 100, so requests with more than 100 postIds
can miss matches. Update this method to process postIds in chunks of 100 (or
smaller), run the query for each chunk using the same userId and contractId
filters, and merge the results into the returned Set<string> so all liked posts
are captured.

In `@lib/services/repost-service.ts`:
- Around line 177-187: The getUserRepostedPostIds helper is artificially capped
at 100 IDs, which can miss reposts for larger input batches. Update this method
to chunk the postIds array and query each chunk separately, then merge the
results into the returned Set<string>. Keep the existing query shape in
RepostService.sdk()/documents.query and preserve the early return for empty
input.

---

Outside diff comments:
In `@lib/services/tip-service.ts`:
- Around line 332-351: createTipPost in tip-service.ts is swallowing all
failures by logging and returning only false, which prevents callers like
tip-modal.tsx from telling insufficient YAPP apart from network or validation
errors. Update createTipPost to propagate a reason along with the success state,
such as an error code/message or a typed result, and preserve the original error
details from the catch block instead of collapsing everything to a generic
boolean. Keep the existing flow in replyService.createReply and the logger.error
path, but make the return value from createTipPost specific enough for the UI to
choose the correct user-facing message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4148c772-b026-4b5b-8cc8-d0cb054730d1

📥 Commits

Reviewing files that changed from the base of the PR and between 911ee70 and 5efbafa.

📒 Files selected for processing (15)
  • app/settings/page.tsx
  • components/compose/compose-modal.tsx
  • components/post/post-card.tsx
  • components/post/tip-modal.tsx
  • components/settings/moderation-settings.tsx
  • hooks/use-buy-yapp-modal.ts
  • lib/services/like-service.ts
  • lib/services/pagination-utils.ts
  • lib/services/post-stats-helpers.ts
  • lib/services/reply-service.ts
  • lib/services/repost-service.ts
  • lib/services/sdk-helpers.ts
  • lib/services/tip-service.ts
  • lib/services/token-service.ts
  • scripts/set-yapp-price.mjs
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/settings/page.tsx
  • components/settings/moderation-settings.tsx
  • scripts/set-yapp-price.mjs

Comment thread components/post/tip-modal.tsx
Comment thread lib/services/like-service.ts Outdated
Comment thread lib/services/repost-service.ts Outdated

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Prior finding prior-1 is STILL VALID and carried forward: the JSDoc on getPricePerToken still misattributes the maxTotalCost overspend protection, which now lives in buyYapp's caller-supplied cap. New in latest delta: (a) partial thread-post failures don't route through the new handleInsufficientYapp prompt, and (b) the tip modal's new announcement-failure toast hard-codes '(needs YAPP)' but createTipPost returns false for any thrown error. The Codex retry-cache concern is a real design weakness that this PR amplifies by adding token cost to create-paths, but the caching logic itself pre-exists this PR (introduced in #260) and the most common timeout path (waitForResponse timeout) returns optimistic success without retry, so it's downgraded to a suggestion. Sonnet5's getUserLikedPostIds index-order blocker is not confirmable: the identical postId in [...] + $ownerId == pattern is used in the pre-existing getLike (feed page) and in the shipped batch fetch, so if the pattern silently failed likes would already be broken in production — dropping as speculative.

🟡 3 suggestion(s) | 💬 1 nitpick(s)

Findings not posted inline (2)

These findings could not be anchored to the current diff, but they are still part of this review.

  • [SUGGESTION] components/compose/compose-modal.tsx:888-893: Thread partial failures bypass the new Buy-YAPP prompt — The new insufficient-YAPP handling only runs in the outer catch at line 906 — handleInsufficientYapp(error, ...). That path fires when the FIRST post fails. In the partial-success branch here, a thread has created one or more posts and then failed on a later token-paid reply; if that later failure is due to insufficient YAPP, the code only shows a generic toast.error(...failed: ${errorMsg}. Press Post to retry.) and never opens the Buy-YAPP modal. That leaves the user without the intended recovery path even though the PR's stated goal is surfacing insufficient-balance prompts across post/reply flows. Threading `handleInsufficientYapp(fail...
  • [SUGGESTION] lib/services/state-transition-service.ts:350-413: Retry cache miss on regenerated document id — duplicate token-paid creates possible on broadcast-error retry — The pending-ST cache is keyed by documentId, but for normal post/reply/like/repost creates callers do not pass options.documentId or options.entropy, so documentBuilderService.buildDocumentForCreate (line 350) generates fresh entropy and a fresh id on every call. If retryPostCreation re-invokes createDocument after a broadcastStateTransition throws a retryable error (line 501), the second attempt computes a different documentId, misses the cache load at line 364, and signs a brand-new ST with a new nonce. If the original broadcast actually landed on Platform despite the thrown error, the retry creates a duplicate document — an...
Carried-forward findings already raised (1)

These findings were not re-posted as new inline comments because an existing review thread already covers them.

  • [NITPICK] (deduped existing open thread) lib/services/token-service.ts:64-67: Stale doc comment on getPricePerToken after the maxTotalCost fix (carried forward from prior-1) — STILL VALID from prior-1 at head 5efbafa. The JSDoc on getPricePerToken still reads that it is fetched fresh each call so maxTotalCost never uses a stale price. That was accurate when buyYapp fetched the price internally right before signing, but the safety property was moved: buyYapp(buyerId, amount, maxTotalCost) (lines 84-113) now takes the cap from the caller — the UI-confirmed quote — and its JSDoc correctly explains that a mid-flight price bump is rejected on-chain because the signed cap is the one the user approved. Leaving this comment risks misleading a future caller into thinking a fresh call here alone is what keeps purcha...
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `components/post/tip-modal.tsx`:
- [SUGGESTION] components/post/tip-modal.tsx:211-213: Tip-announcement failure toast blames YAPP for every failure mode
  The new toast renders `Tip sent — but the public tip note couldn't be posted (needs YAPP).` whenever `result.announcementPosted === false`. But `TipService.createTipPost` (lib/services/tip-service.ts:326-351) sets that flag to `false` on *any* thrown error — the try/catch swallows everything from `replyService.createReply`, not just insufficient-YAPP failures. A DAPI timeout, signer error, or any other network/contract-level rejection during the announcement reply will surface as a false claim that the user needs YAPP when they may hold plenty, which is misleading and unactionable. The tip-service already has `isInsufficientTokenError` — either narrow `announcementPosted:false` to insufficient-YAPP failures only (returning true on other errors since the tip transfer itself succeeded and the announcement is best-effort), or thread an `announcementError` code back up and let the modal pick the right toast wording. Minor because the money-path (credit transfer) is unaffected, but the wording is user-facing on a paid flow.

In `components/compose/compose-modal.tsx`:
- [SUGGESTION] components/compose/compose-modal.tsx:888-893: Thread partial failures bypass the new Buy-YAPP prompt
  The new insufficient-YAPP handling only runs in the outer catch at line 906 — `handleInsufficientYapp(error, ...)`. That path fires when the FIRST post fails. In the partial-success branch here, a thread has created one or more posts and then failed on a later token-paid reply; if that later failure is due to insufficient YAPP, the code only shows a generic `toast.error(...failed: ${errorMsg}. Press Post to retry.)` and never opens the Buy-YAPP modal. That leaves the user without the intended recovery path even though the PR's stated goal is surfacing insufficient-balance prompts across post/reply flows. Threading `handleInsufficientYapp(failureError, 'You need YAPP to finish posting this thread. Buy some to continue.')` into this branch (falling through to the current toast when it doesn't match) would cover the partial-thread case without changing the retry semantics.

In `lib/services/state-transition-service.ts`:
- [SUGGESTION] lib/services/state-transition-service.ts:350-413: Retry cache miss on regenerated document id — duplicate token-paid creates possible on broadcast-error retry
  The pending-ST cache is keyed by `documentId`, but for normal post/reply/like/repost creates callers do not pass `options.documentId` or `options.entropy`, so `documentBuilderService.buildDocumentForCreate` (line 350) generates fresh entropy and a fresh id on every call. If `retryPostCreation` re-invokes `createDocument` after a `broadcastStateTransition` throws a retryable error (line 501), the second attempt computes a different `documentId`, misses the cache load at line 364, and signs a brand-new ST with a new nonce. If the original broadcast actually landed on Platform despite the thrown error, the retry creates a duplicate document — and under this PR's v4 tokenomics each duplicate now costs YAPP twice (posts, replies, likes, and reposts all have `tokenCost.create`). The caching design itself pre-exists this PR (introduced in #260 `fix: prevent double-posting on DAPI timeout`), so this is not a new bug — and the more common `waitForResponse` timeout path returns optimistic `success:true, confirmed:false` without throwing (lines 518-538), which means retries only fire on rarer broadcast-side errors. Still, this PR meaningfully raises the cost of the failure mode (duplicate content + double YAPP debit), so it's worth revisiting: either persist the entropy into the retry closure (compute `entropy` once in the caller and pass it via `options.entropy` on every attempt), or key the cache on a stable per-operation identity (contract+type+ownerId+contentHash) rather than the fresh docId. If out of scope, at minimum consider disabling `retryPostCreation`'s auto-retry for token-paid creates and letting the outer UI drive a user-initiated retry through the cache path.

In `lib/services/token-service.ts`:
- [NITPICK] lib/services/token-service.ts:64-67: Stale doc comment on getPricePerToken after the maxTotalCost fix (carried forward from prior-1)
  STILL VALID from prior-1 at head 5efbafa8. The JSDoc on `getPricePerToken` still reads that it is fetched fresh each call so `maxTotalCost` never uses a stale price. That was accurate when `buyYapp` fetched the price internally right before signing, but the safety property was moved: `buyYapp(buyerId, amount, maxTotalCost)` (lines 84-113) now takes the cap from the caller — the UI-confirmed quote — and its JSDoc correctly explains that a mid-flight price bump is rejected on-chain because the signed cap is the one the user approved. Leaving this comment risks misleading a future caller into thinking a fresh call here alone is what keeps purchases safe; a new call site that skips threading a user-confirmed cap through `buyYapp` would reintroduce the original overspend window. The latest delta touched other parts of this file (getBalance rework at 57-62, the INSUFFICIENT_CREDITS/INVALID_KEY matcher refinements) but did not touch lines 64-67.

Comment thread components/post/tip-modal.tsx
PastaPastaPasta and others added 3 commits July 2, 2026 11:18
…al opens

The on-chain error reads 'does not have enough balance for token X: required N, actual 0' which none of the isInsufficientTokenError patterns matched, so failed posts showed the raw broadcast error instead of the Buy-YAPP prompt. Also route mid-thread partial failures through the same handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a 'See action costs' disclosure listing the per-action YAPP prices from the contract (post 10, reply 3, like/repost 1) and a dynamic 'covers about N posts, M replies, or K likes' line under the cost on both the input and confirm steps, so buyers know how long a bundle lasts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y is HIGH

Drive rejects token direct purchases signed with a HIGH auth key (Invalid public key security level HIGH, requires CRITICAL). buyYapp now matches the stored WIF against CRITICAL keys only and returns NEEDS_CRITICAL_KEY when it can't; the Buy-YAPP modal then shows an Authorize Purchase step where the user pastes their CRITICAL key, which signs locally and is never stored. The key is cleared on close, back, success, and any exit from the authorize flow so a retry can never silently reuse it. Freeze/unfreeze authority actions keep their existing CRITICAL|HIGH matching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread package.json
"@blocknote/mantine": "^0.47.0",
"@blocknote/react": "^0.47.0",
"@dashevo/evo-sdk": "3.1.0-dev.8",
"@dashevo/evo-sdk": "^4.0.0-rc.2",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use actual release version. And pin (so you don't have to worry about breaking changes in a patch version or smth)?

Suggested change
"@dashevo/evo-sdk": "^4.0.0-rc.2",
"@dashevo/evo-sdk": "4.0.0",

…p warnings

Reset and stale-guard the sidebar YAPP balance on identity switch; chunk per-user like/repost/bookmark lookups and grouped counts to respect Platform's 100-value in-clause cap; rethrow from identityService.getBalance so fetch failures aren't treated as a zero balance (call sites hardened); return structured announcement-failure detail from sendTip so the tip toast only blames missing YAPP when that was the cause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/services/repost-service.ts (1)

231-253: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid a single 100-row byPost query here. load-for-you-feed only passes 20 IDs, so chunking by postIds won’t help. The limit: 100 on this non-unique query can still drop reposts for a popular batch, which leaves some posts without repostedBy data. Fetch one/latest repost per post instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/repost-service.ts` around lines 231 - 253, The current
getRepostsByPostIds batch lookup in RepostService uses a single non-unique
byPost query with a hard limit of 100, which can omit reposts for some postIds.
Update getRepostsByPostIds to fetch one/latest repost per postId instead of
relying on the batch limit, and keep the existing mapping/normalizeSDKResponse
flow inside the method so every requested post gets its repostedBy data when
available.
components/token/buy-yapp-modal.tsx (1)

108-134: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard handleBuy against double-submission.

handleBuy has no re-entrancy check, and the "Confirm & Buy" (line 283) and "Authorize & Buy" (line 324) buttons aren't disabled while an invocation is in-flight — only state === 'processing' removes them from the DOM after a re-render. A double-click or rapid repeated Enter before that re-render commits could fire handleBuy twice, potentially signing and broadcasting two separate directPurchase transitions for real DASH credits. This is unlikely in practice (React typically flushes the state update before the next click registers) but the fix is trivial and the downside (duplicate spend) is high-impact enough to be worth a defensive guard.

🛡️ Proposed fix
   const handleBuy = async () => {
-    if (!user || costCredits === null) return
+    if (!user || costCredits === null || state === 'processing') return
     setState('processing')

Also applies to: 268-286, 288-329

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@lib/services/repost-service.ts`:
- Around line 231-253: The current getRepostsByPostIds batch lookup in
RepostService uses a single non-unique byPost query with a hard limit of 100,
which can omit reposts for some postIds. Update getRepostsByPostIds to fetch
one/latest repost per postId instead of relying on the batch limit, and keep the
existing mapping/normalizeSDKResponse flow inside the method so every requested
post gets its repostedBy data when available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08f985df-c618-4174-9e33-cd8a49e18abc

📥 Commits

Reviewing files that changed from the base of the PR and between 5efbafa and 1248873.

📒 Files selected for processing (13)
  • components/compose/compose-modal.tsx
  • components/layout/sidebar.tsx
  • components/post/tip-modal.tsx
  • components/token/buy-yapp-modal.tsx
  • components/token/yapp-balance-item.tsx
  • lib/error-utils.ts
  • lib/services/bookmark-service.ts
  • lib/services/identity-service.ts
  • lib/services/like-service.ts
  • lib/services/pagination-utils.ts
  • lib/services/repost-service.ts
  • lib/services/tip-service.ts
  • lib/services/token-service.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • lib/error-utils.ts
  • components/post/tip-modal.tsx
  • components/token/yapp-balance-item.tsx
  • lib/services/like-service.ts
  • lib/services/pagination-utils.ts
  • lib/services/token-service.ts

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Latest delta (5efbafa..1248873) cleanly resolves prior-2 and prior-3: tip-modal now distinguishes INSUFFICIENT_YAPP from generic announcement failures, and compose-modal routes thread partial-failures through the Buy-YAPP prompt. New CRITICAL-key entry flow for YAPP purchases is well-scoped. Only carry-forward: prior-1 stale JSDoc on getPricePerToken (nitpick). Prior-4 (retry cache regenerates documentId) remains an intentionally deferred pre-existing design.

Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed); verifier: opus

💬 1 nitpick(s)

Comment on lines +64 to +67
/**
* Price in credits to buy one YAPP (the SetPrices tier value). Null if not for
* sale. Fetched fresh each call so `maxTotalCost` never uses a stale price.
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Stale JSDoc on getPricePerToken misattributes maxTotalCost overspend protection

Carried forward from prior-1 — unchanged at head 1248873. The comment reads that getPricePerToken is fetched fresh each call "so maxTotalCost never uses a stale price." That was true when buyYapp fetched the price internally, but the safety property now lives in buyYapp(buyerId, amount, maxTotalCost, criticalKeyWif?) (lines 95-121): the cap is caller-supplied (the UI-confirmed quote) and enforced on-chain, and its JSDoc at lines 84-94 correctly documents this. Leaving the stale note here risks misleading a future caller into assuming a fresh call to getPricePerToken alone is what keeps purchases safe; a new call site that skipped threading a user-confirmed cap through buyYapp would reintroduce the overspend window. The rest of this file was touched substantially (getBalance rethrow, NEEDS_CRITICAL_KEY marker, getAuthSigner overrideWif) but lines 64-67 were not.

Suggested change
/**
* Price in credits to buy one YAPP (the SetPrices tier value). Null if not for
* sale. Fetched fresh each call so `maxTotalCost` never uses a stale price.
*/
/**
* Price in credits to buy one YAPP (the SetPrices tier value). Null if not for
* sale. Not cached, since the price can change at any time. Overspend
* protection lives in `buyYapp`, which enforces the caller-supplied
* `maxTotalCost` cap on-chain; callers holding a quote across a user
* confirmation should re-quote if the confirmation window may be long.
*/

source: ['claude', 'codex']

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Carried-forward: prior-1 (stale JSDoc on getPricePerToken at lib/services/token-service.ts:64-67) is STILL VALID — the comment still credits freshness of getPricePerToken with overspend protection, when the actual cap enforcement lives in buyYapp via caller-supplied maxTotalCost. New in latest delta (bf695e1): sonnet5 flagged the void Promise.all(...) added at components/token/buy-yapp-modal.tsx:60 to silence @typescript-eslint/no-floating-promises — this pattern is explicitly listed as prohibited in the project's CLAUDE.md, which requires a proper .catch() instead. Both are suggestion-level; no blockers.

Source: reviewers: opus (general), claude-sonnet-5 (general), gpt-5.5[high] (general, failed); verifier: opus

🟡 1 suggestion(s) | 💬 1 nitpick(s)

Carried-forward findings already raised (1)

These findings were not re-posted as new inline comments because an existing review thread already covers them.

  • [NITPICK] (deduped existing open thread) lib/services/token-service.ts:64-67: Stale JSDoc on getPricePerToken misattributes maxTotalCost overspend protection — Carried forward from prior review — lines 64-67 are unchanged at head bf695e1. The comment reads that getPricePerToken is fetched fresh each call "so maxTotalCost never uses a stale price." That framing is misleading: the actual overspend protection lives in `buyYapp(buyerId, amount, maxTota...
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `components/token/buy-yapp-modal.tsx`:
- [SUGGESTION] components/token/buy-yapp-modal.tsx:60-64: `void` prefix silences floating-promise lint in a way the project's CLAUDE.md prohibits
  The mount effect now uses `void Promise.all([...]).finally(...)` to silence the `@typescript-eslint/no-floating-promises` warn-level rule. The project's own CLAUDE.md explicitly forbids this suppression pattern — it lists `void someAsyncFn()` among the things NOT to do, and directs contributors to `add proper .catch() error handling` instead. Each inner promise already has its own `.catch()`, so the outer chain just needs `.catch(() => {})` (or restructuring as an async IIFE with a catch) to satisfy the rule without using the disallowed shortcut. Small change, but the guideline is explicit enough that it's worth honoring here so future contributors don't take this as precedent.

Comment on lines +60 to +64
void Promise.all([
tokenService.getBalance(user.identityId).then(setYappBalance).catch(() => setYappBalance(null)),
identityService.getBalance(user.identityId).then(b => setCreditBalance(b.confirmed)).catch(() => setCreditBalance(null)),
tokenService.getPricePerToken().then(setPricePerToken).catch(() => setPricePerToken(null)),
]).finally(() => setLoading(false))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: void prefix silences floating-promise lint in a way the project's CLAUDE.md prohibits

The mount effect now uses void Promise.all([...]).finally(...) to silence the @typescript-eslint/no-floating-promises warn-level rule. The project's own CLAUDE.md explicitly forbids this suppression pattern — it lists void someAsyncFn() among the things NOT to do, and directs contributors to add proper .catch() error handling instead. Each inner promise already has its own .catch(), so the outer chain just needs .catch(() => {}) (or restructuring as an async IIFE with a catch) to satisfy the rule without using the disallowed shortcut. Small change, but the guideline is explicit enough that it's worth honoring here so future contributors don't take this as precedent.

Suggested change
void Promise.all([
tokenService.getBalance(user.identityId).then(setYappBalance).catch(() => setYappBalance(null)),
identityService.getBalance(user.identityId).then(b => setCreditBalance(b.confirmed)).catch(() => setCreditBalance(null)),
tokenService.getPricePerToken().then(setPricePerToken).catch(() => setPricePerToken(null)),
]).finally(() => setLoading(false))
Promise.all([
tokenService.getBalance(user.identityId).then(setYappBalance).catch(() => setYappBalance(null)),
identityService.getBalance(user.identityId).then(b => setCreditBalance(b.confirmed)).catch(() => setCreditBalance(null)),
tokenService.getPricePerToken().then(setPricePerToken).catch(() => setPricePerToken(null)),
]).finally(() => setLoading(false)).catch(() => {})

source: ['claude']

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.

3 participants