Skip to content

feat(affiliate): add trader activity endpoint#218

Merged
kernelwhisperer merged 5 commits intomainfrom
feat/affiliate-trader-activity-table
May 6, 2026
Merged

feat(affiliate): add trader activity endpoint#218
kernelwhisperer merged 5 commits intomainfrom
feat/affiliate-trader-activity-table

Conversation

@kernelwhisperer
Copy link
Copy Markdown
Contributor

@kernelwhisperer kernelwhisperer commented Apr 29, 2026

Summary

Adds GET /affiliate/trader-activity/:address, backed by Dune, returning recent trader activity with token symbols,
volume/eligibility data, and cache metadata.

To Test

  • Set DUNE_QUERY_ID_TRADER_ACTIVITY=6561349 in .env
  • Use the Dune query for reference/debugging: https://dune.com/queries/6561349
  • Call the endpoint with a trader address and verify rows match Dune output, e.g.: 0x9425596dc3a37aF56D8D531d74c22EB675ebBd11

Deployment

DUNE_QUERY_ID_TRADER_ACTIVITY should be set to 6561349 on staging and 6683651 on prod

Summary by CodeRabbit

  • New Features

    • Added an affiliate trader activity API endpoint and support for fetching detailed trader activity metrics.
  • Enhancements

    • Extended affiliate stats to include trader-activity results and per-address caching for faster responses.
  • Bug Fixes

    • Improved error logging for CMS-related error handling.
  • Tests

    • Added comprehensive tests for trader activity: data mapping, caching, edge cases, and error handling.
  • Chores

    • Added commented environment variables for Dune query IDs to config examples.

@kernelwhisperer kernelwhisperer self-assigned this Apr 29, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c71fe07b-0a28-4825-be79-385bea29c406

📥 Commits

Reviewing files that changed from the base of the PR and between 6fe97b3 and dafce86.

📒 Files selected for processing (1)
  • libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts

📝 Walkthrough

Walkthrough

Adds trader-activity support: new DUNE query config and .env example entry, types and raw/type guards, normalization helpers, service implementation with per-address caching and getTraderActivity, guarded API GET route with schemas, tests for behavior and errors, and a one-line logger.error addition in a ref-codes handler.

Changes

Trader Activity Feature

Layer / File(s) Summary
Config
.env.example, libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts
Adds commented DUNE_QUERY_ID_TRADER_ACTIVITY example entry and reads/validates DUNE_QUERY_ID_TRADER_ACTIVITY in config; includes value in returned query IDs.
Types
libs/services/src/AffiliateStatsService/AffiliateStatsService.ts, .../AffiliateStatsService.types.ts
Defines TraderActivityRow<T>, TraderActivityResult; adds TraderActivityRowRaw type and extends AffiliateStatsService interface with getTraderActivity(address): Promise<TraderActivityResult>.
Validation & Normalization
libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts
Adds BLOCKCHAIN_TO_CHAIN_ID mapping, isTraderActivityRowRaw type guard, normalizeTraderActivityRow transformer, getChainId, and toDecimalString helpers; exports new utilities.
Implementation
libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts
Adds getTraderActivity(address) with normalized address keys, per-address caching, query execution, type-guarded row mapping, filtering by normalized trader address, and a 50-row default limit; updates getTraderStats/getAffiliateStats to use normalized keys and shared cached query flow.
API Route & Schemas
apps/api/src/app/routes/affiliate/trader-activity/_address/index.ts, .../traderActivity.schemas.ts
New guarded Fastify GET route for /affiliate/trader-activity/:address with paramsSchema, traderActivityRowSchema, responseSchema, and errorSchema; resolves AffiliateStatsService and returns TraderActivityResult or 500 on error.
Tests
libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.spec.ts
Adds MockDuneRepository, helpers to synthesize Dune results, and tests for fetching/mapping rows and lastUpdatedAt, empty results, caching behavior, and error on unsupported blockchain.

Error Logging Enhancement

Layer / File(s) Summary
Logging
apps/api/src/app/routes/ref-codes/_code/index.ts
Inserts a logger.error call at the start of handleCmsError to log the error before existing CMS error handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant APIRoute as API Route (trader-activity)
    participant Service as AffiliateStatsServiceImpl
    participant Cache as Address Cache
    participant Dune as Dune Analytics

    Client->>APIRoute: GET /affiliate/trader-activity/:address
    APIRoute->>Service: getTraderActivity(address)
    Service->>Service: normalize address (getAddressKey)
    Service->>Cache: check cache for key
    alt cache hit
        Cache-->>Service: cached TraderActivityResult
    else cache miss
        Service->>Dune: execute latest traderActivity query
        Dune-->>Service: raw rows + lastUpdatedAt
        Service->>Service: isTraderActivityRowRaw & normalizeTraderActivityRow
        Service->>Service: filter by normalized address & limit 50
        Service->>Cache: store TraderActivityResult
    end
    Service-->>APIRoute: TraderActivityResult
    APIRoute-->>Client: 200 JSON
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through Dune where data threads weave,
Normalized hops and caches up my sleeve,
Rows trimmed and mapped with chain ids in sight,
Tests guard the burrow through day and night,
Trader trails now gleam in rabbit-light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(affiliate): add trader activity endpoint' clearly and specifically summarizes the main change—adding a new trader activity endpoint to the affiliate module.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/affiliate-trader-activity-table

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

@kernelwhisperer kernelwhisperer force-pushed the feat/affiliate-trader-activity-table branch from ae1cccd to 07a32da Compare May 5, 2026 09:52
@kernelwhisperer
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@kernelwhisperer kernelwhisperer marked this pull request as ready for review May 5, 2026 10:29
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kernelwhisperer kernelwhisperer requested a review from a team May 5, 2026 10:29
Comment thread libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts Outdated
@kernelwhisperer kernelwhisperer requested a review from a team May 5, 2026 14:49
Base automatically changed from fix/auto-fix-lint to main May 5, 2026 15:24
@kernelwhisperer kernelwhisperer force-pushed the feat/affiliate-trader-activity-table branch from bf709b8 to 3aeb458 Compare May 6, 2026 06:32
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts (1)

141-150: 💤 Low value

Prefer === undefined over a truthy check in getChainId.

if (!chainId) would also treat a chain ID of 0 as "unsupported". No SupportedChainId today is 0, so this is not a real bug, but === undefined is more robust to enum changes and is consistent with how a Record lookup is normally handled.

-  const chainId = BLOCKCHAIN_TO_CHAIN_ID[normalizedBlockchain]
-
-  if (!chainId) {
+  const chainId = BLOCKCHAIN_TO_CHAIN_ID[normalizedBlockchain]
+
+  if (chainId === undefined) {
     throw new Error(`Unsupported affiliate trader activity blockchain: ${blockchain}`)
   }
🤖 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 `@libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts`
around lines 141 - 150, The lookup in getChainId uses a truthy check which would
misclassify a valid chainId of 0 as unsupported; change the conditional that
checks BLOCKCHAIN_TO_CHAIN_ID[normalizedBlockchain] to explicitly compare to
undefined (e.g., if (chainId === undefined)) so only missing entries trigger the
error, keeping the Error throw message and return of SupportedChainId intact;
update the references around getChainId and BLOCKCHAIN_TO_CHAIN_ID accordingly.
libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts (2)

70-87: 💤 Low value

Redundant error logging.

getCachedQuery already logs Affiliate stats Dune query failed (...) on the same error before rethrowing (line 177). The outer try/catch here re-logs essentially the same failure with a different cache-key prefix, so each Dune failure for trader activity will produce two error log entries. Either drop the outer try/catch and let the error propagate, or remove the inner log for paths that have a more specific outer handler.

♻️ Possible simplification
-    try {
-      const { rows: duneRows, lastUpdatedAt } = await this.getCachedQuery<TraderActivityRowRaw, TraderActivityDuneRow>({
-        cacheKey: 'affiliate-trader-activity',
-        queryId: getDuneQueryIds().traderActivity,
-        typeAssertion: isTraderActivityRowRaw,
-        mapRow: normalizeTraderActivityRow,
-      })
-      const filteredRows = duneRows.filter((row) => getAddressKey(row.trader_address) === normalizedAddress)
-      const rows = filteredRows.slice(0, DEFAULT_TRADER_ACTIVITY_LIMIT)
-
-      this.setCache(cacheKey, rows, lastUpdatedAt)
-
-      return { rows, lastUpdatedAt }
-    } catch (error) {
-      logger.error({ error }, `Affiliate trader activity Dune query failed (${cacheKey}).`)
-      throw error
-    }
+    const { rows: duneRows, lastUpdatedAt } = await this.getCachedQuery<TraderActivityRowRaw, TraderActivityDuneRow>({
+      cacheKey: 'affiliate-trader-activity',
+      queryId: getDuneQueryIds().traderActivity,
+      typeAssertion: isTraderActivityRowRaw,
+      mapRow: normalizeTraderActivityRow,
+    })
+    const filteredRows = duneRows.filter((row) => getAddressKey(row.trader_address) === normalizedAddress)
+    const rows = filteredRows.slice(0, DEFAULT_TRADER_ACTIVITY_LIMIT)
+
+    this.setCache(cacheKey, rows, lastUpdatedAt)
+
+    return { rows, lastUpdatedAt }
🤖 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 `@libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts` around
lines 70 - 87, The outer try/catch around the block that calls
getCachedQuery(...) causes duplicate error logs because getCachedQuery already
logs failures; remove the outer try/catch (and its logger.error + rethrow) in
the method in AffiliateStatsServiceImpl so errors from getCachedQuery propagate
naturally, leaving the existing logic that filters with getAddressKey, slices
with DEFAULT_TRADER_ACTIVITY_LIMIT, calls setCache(cacheKey, rows,
lastUpdatedAt), and returns { rows, lastUpdatedAt } intact.

56-87: ⚖️ Poor tradeoff

Operational note: full-dataset scan for a single-trader query.

getCachedQuery paginates through the entire trader-activity Dune result (up to DUNE_MAX_ROWS = 1_000_000) before this method filters down to the requested address. The shared underlying cache amortises the cost, but on a cold cache a single request triggers a full-dataset fetch and walks every row in memory. As the dataset grows you may want to push the address filter into the Dune query (parameterised query) or into a server-side index, so per-request work scales with the trader's activity instead of the global dataset.

🤖 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 `@libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts` around
lines 56 - 87, getTraderActivity currently fetches the entire Dune
trader-activity result then filters in-memory causing full-dataset scans; change
it to push the trader_address into the Dune query and cache key so the query
returns only that trader's rows. Update the call to getCachedQuery (used in
getTraderActivity) to pass a per-trader cacheKey (e.g., include
normalizedAddress) and supply query parameters (trader_address:
normalizedAddress) to the Dune query identified by
getDuneQueryIds().traderActivity, keep typeAssertion isTraderActivityRowRaw and
mapRow normalizeTraderActivityRow, and still apply DEFAULT_TRADER_ACTIVITY_LIMIT
to the returned rows; remove the large in-memory filter over duneRows so
cold-cache requests no longer paginate DUNE_MAX_ROWS.
libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts (1)

1-34: 💤 Low value

Consider extracting the env-var parsing into a helper.

The "read, error if missing, parseInt, error if NaN" pattern is now duplicated three times and will grow as more Dune queries are added. A small helper would centralize the validation messages and reduce noise.

♻️ Possible refactor
+function readIntEnv(name: string): number {
+  const raw = process.env[name]
+  if (!raw) {
+    throw new Error(`${name} is not set`)
+  }
+  const value = Number.parseInt(raw, 10)
+  if (Number.isNaN(value)) {
+    throw new Error(`${name} must be an integer`)
+  }
+  return value
+}
+
 export function getDuneQueryIds(): {
   traderStats: number
   traderActivity: number
   affiliateStats: number
 } {
-  const traderRaw = process.env.DUNE_QUERY_ID_TRADER_STATS
-  if (!traderRaw) {
-    throw new Error('DUNE_QUERY_ID_TRADER_STATS is not set')
-  }
-  const traderStats = Number.parseInt(traderRaw, 10)
-  if (Number.isNaN(traderStats)) {
-    throw new Error('DUNE_QUERY_ID_TRADER_STATS must be an integer')
-  }
-
-  const traderActivityRaw = process.env.DUNE_QUERY_ID_TRADER_ACTIVITY
-  if (!traderActivityRaw) {
-    throw new Error('DUNE_QUERY_ID_TRADER_ACTIVITY is not set')
-  }
-  const traderActivity = Number.parseInt(traderActivityRaw, 10)
-  if (Number.isNaN(traderActivity)) {
-    throw new Error('DUNE_QUERY_ID_TRADER_ACTIVITY must be an integer')
-  }
-
-  const affiliateRaw = process.env.DUNE_QUERY_ID_AFFILIATE_STATS
-  if (!affiliateRaw) {
-    throw new Error('DUNE_QUERY_ID_AFFILIATE_STATS is not set')
-  }
-  const affiliateStats = Number.parseInt(affiliateRaw, 10)
-  if (Number.isNaN(affiliateStats)) {
-    throw new Error('DUNE_QUERY_ID_AFFILIATE_STATS must be an integer')
-  }
-
-  return { traderStats, traderActivity, affiliateStats }
+  return {
+    traderStats: readIntEnv('DUNE_QUERY_ID_TRADER_STATS'),
+    traderActivity: readIntEnv('DUNE_QUERY_ID_TRADER_ACTIVITY'),
+    affiliateStats: readIntEnv('DUNE_QUERY_ID_AFFILIATE_STATS'),
+  }
 }
🤖 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 `@libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts`
around lines 1 - 34, Extract the repeated "read env, throw if missing, parseInt,
throw if NaN" logic into a small helper (e.g., getEnvInt or parseEnvInt) and
update getDuneQueryIds to call that helper for each variable
(DUNE_QUERY_ID_TRADER_STATS, DUNE_QUERY_ID_TRADER_ACTIVITY,
DUNE_QUERY_ID_AFFILIATE_STATS); the helper should accept the env var name, throw
the same missing/invalid-int errors as currently used, and return a number so
getDuneQueryIds simply returns { traderStats:
getEnvInt('DUNE_QUERY_ID_TRADER_STATS'), traderActivity:
getEnvInt('DUNE_QUERY_ID_TRADER_ACTIVITY'), affiliateStats:
getEnvInt('DUNE_QUERY_ID_AFFILIATE_STATS') }.
libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.spec.ts (1)

114-122: 💤 Low value

jest.resetModules() has no effect here and can be removed.

All module imports are static (resolved at file load time), so resetModules() never causes AffiliateStatsServiceImpl or its dependencies to re-evaluate. The call is misleading and unnecessary; setting process.env before each new AffiliateStatsServiceImpl(...) instantiation is sufficient.

♻️ Proposed cleanup
  beforeEach(() => {
-   jest.resetModules()
    process.env = {
      ...originalEnv,
      DUNE_QUERY_ID_TRADER_STATS: '101',
      DUNE_QUERY_ID_TRADER_ACTIVITY: '102',
      DUNE_QUERY_ID_AFFILIATE_STATS: '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 `@libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.spec.ts`
around lines 114 - 122, The call to jest.resetModules() in the beforeEach block
is unnecessary because imports are static and it doesn't force re-evaluation of
AffiliateStatsServiceImpl or its dependencies; remove the jest.resetModules()
line and keep only the process.env assignment so tests set
DUNE_QUERY_ID_TRADER_STATS, DUNE_QUERY_ID_TRADER_ACTIVITY, and
DUNE_QUERY_ID_AFFILIATE_STATS before instantiating new
AffiliateStatsServiceImpl(...) (and ensure any test setup relying on module
cache is handled by resetting env or re-instantiating objects, not
resetModules).
apps/api/src/app/routes/affiliate/trader-activity/_address/index.ts (1)

30-39: 💤 Low value

Move DI resolution outside the request handler.

apiContainer.get(...) is called on every request for a service registered as a singleton. Resolving it once at plugin scope fails loudly at startup if the binding is missing, instead of being silently swallowed as a 500.

♻️ Proposed refactor
   if (!isDuneEnabled) {
     fastify.log.warn('DUNE_API_KEY is not set. Skipping affiliate trader activity endpoint.')
     return
   }

+  const affiliateStatsService = apiContainer.get<AffiliateStatsService>(affiliateStatsServiceSymbol)
+
   fastify.get<{ Params: ParamsSchema; Reply: ResponseSchema }>(
     '/',
     { ... },
     async function (request, reply) {
       try {
-        const affiliateStatsService = apiContainer.get<AffiliateStatsService>(affiliateStatsServiceSymbol)
         const result = await affiliateStatsService.getTraderActivity(request.params.address)
         return reply.send(result)
       } catch (error) {
🤖 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 `@apps/api/src/app/routes/affiliate/trader-activity/_address/index.ts` around
lines 30 - 39, The handler resolves the singleton via apiContainer.get on every
request which hides missing bindings at runtime; move the DI resolution out of
the async request handler into the surrounding module/plugin scope so the
container resolves AffiliateStatsService (using affiliateStatsServiceSymbol and
the AffiliateStatsService type) once at startup and the handler (the async
function(request, reply)) simply calls
affiliateStatsService.getTraderActivity(request.params.address); this makes
resolution fail loudly at startup if the binding is missing and avoids
per-request lookups.
🤖 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 `@libs/services/src/AffiliateStatsService/AffiliateStatsService.ts`:
- Around line 16-56: TraderActivityRow and TraderActivityDuneRow are identical;
remove duplication by consolidating them into a single exported type (e.g., keep
TraderActivityRow and make TraderActivityDuneRow an alias, or delete one and
export the remaining), then update usages in
AffiliateStatsServiceImpl.getTraderActivity to use the single type and remove
the cast on cached.rows (the cast "as TraderActivityRow[]" should no longer be
needed). Ensure all imports across the codebase reference the consolidated
symbol (TraderActivityRow or the chosen alias) and update any tests/types that
referenced the removed duplicate.

In `@libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts`:
- Around line 77-78: filteredRows are sliced without a client-side sort so
"recent trader activity" can be inconsistent; in AffiliateStatsServiceImpl
locate where filteredRows is computed (variable filteredRows) and sort it by
creation_date in descending order before taking the first
DEFAULT_TRADER_ACTIVITY_LIMIT entries (e.g., perform a compare on
row.creation_date -> sort((a,b)=>
b.creation_date.localeCompare(a.creation_date)) or parse to Date then compare)
so rows = filteredRows.slice(...) becomes rows = filteredRows.sort(/*desc by
creation_date*/).slice(0, DEFAULT_TRADER_ACTIVITY_LIMIT).

---

Nitpick comments:
In `@apps/api/src/app/routes/affiliate/trader-activity/_address/index.ts`:
- Around line 30-39: The handler resolves the singleton via apiContainer.get on
every request which hides missing bindings at runtime; move the DI resolution
out of the async request handler into the surrounding module/plugin scope so the
container resolves AffiliateStatsService (using affiliateStatsServiceSymbol and
the AffiliateStatsService type) once at startup and the handler (the async
function(request, reply)) simply calls
affiliateStatsService.getTraderActivity(request.params.address); this makes
resolution fail loudly at startup if the binding is missing and avoids
per-request lookups.

In `@libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts`:
- Around line 1-34: Extract the repeated "read env, throw if missing, parseInt,
throw if NaN" logic into a small helper (e.g., getEnvInt or parseEnvInt) and
update getDuneQueryIds to call that helper for each variable
(DUNE_QUERY_ID_TRADER_STATS, DUNE_QUERY_ID_TRADER_ACTIVITY,
DUNE_QUERY_ID_AFFILIATE_STATS); the helper should accept the env var name, throw
the same missing/invalid-int errors as currently used, and return a number so
getDuneQueryIds simply returns { traderStats:
getEnvInt('DUNE_QUERY_ID_TRADER_STATS'), traderActivity:
getEnvInt('DUNE_QUERY_ID_TRADER_ACTIVITY'), affiliateStats:
getEnvInt('DUNE_QUERY_ID_AFFILIATE_STATS') }.

In `@libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts`:
- Around line 141-150: The lookup in getChainId uses a truthy check which would
misclassify a valid chainId of 0 as unsupported; change the conditional that
checks BLOCKCHAIN_TO_CHAIN_ID[normalizedBlockchain] to explicitly compare to
undefined (e.g., if (chainId === undefined)) so only missing entries trigger the
error, keeping the Error throw message and return of SupportedChainId intact;
update the references around getChainId and BLOCKCHAIN_TO_CHAIN_ID accordingly.

In `@libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.spec.ts`:
- Around line 114-122: The call to jest.resetModules() in the beforeEach block
is unnecessary because imports are static and it doesn't force re-evaluation of
AffiliateStatsServiceImpl or its dependencies; remove the jest.resetModules()
line and keep only the process.env assignment so tests set
DUNE_QUERY_ID_TRADER_STATS, DUNE_QUERY_ID_TRADER_ACTIVITY, and
DUNE_QUERY_ID_AFFILIATE_STATS before instantiating new
AffiliateStatsServiceImpl(...) (and ensure any test setup relying on module
cache is handled by resetting env or re-instantiating objects, not
resetModules).

In `@libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts`:
- Around line 70-87: The outer try/catch around the block that calls
getCachedQuery(...) causes duplicate error logs because getCachedQuery already
logs failures; remove the outer try/catch (and its logger.error + rethrow) in
the method in AffiliateStatsServiceImpl so errors from getCachedQuery propagate
naturally, leaving the existing logic that filters with getAddressKey, slices
with DEFAULT_TRADER_ACTIVITY_LIMIT, calls setCache(cacheKey, rows,
lastUpdatedAt), and returns { rows, lastUpdatedAt } intact.
- Around line 56-87: getTraderActivity currently fetches the entire Dune
trader-activity result then filters in-memory causing full-dataset scans; change
it to push the trader_address into the Dune query and cache key so the query
returns only that trader's rows. Update the call to getCachedQuery (used in
getTraderActivity) to pass a per-trader cacheKey (e.g., include
normalizedAddress) and supply query parameters (trader_address:
normalizedAddress) to the Dune query identified by
getDuneQueryIds().traderActivity, keep typeAssertion isTraderActivityRowRaw and
mapRow normalizeTraderActivityRow, and still apply DEFAULT_TRADER_ACTIVITY_LIMIT
to the returned rows; remove the large in-memory filter over duneRows so
cold-cache requests no longer paginate DUNE_MAX_ROWS.
🪄 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: 163e8b4a-f9db-4d75-bcc4-0f23b84967cb

📥 Commits

Reviewing files that changed from the base of the PR and between cba32d2 and 3aeb458.

📒 Files selected for processing (10)
  • .env.example
  • apps/api/src/app/routes/affiliate/trader-activity/_address/index.ts
  • apps/api/src/app/routes/affiliate/trader-activity/_address/traderActivity.schemas.ts
  • apps/api/src/app/routes/ref-codes/_code/index.ts
  • libs/services/src/AffiliateStatsService/AffiliateStatsService.config.ts
  • libs/services/src/AffiliateStatsService/AffiliateStatsService.ts
  • libs/services/src/AffiliateStatsService/AffiliateStatsService.types.ts
  • libs/services/src/AffiliateStatsService/AffiliateStatsService.utils.ts
  • libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.spec.ts
  • libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts

Comment thread libs/services/src/AffiliateStatsService/AffiliateStatsService.ts Outdated
Comment thread libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts Outdated
async getTraderActivity(address: string): Promise<TraderActivityResult> {
const normalizedAddress = getAddressKey(address)
const cacheKey = `affiliate-trader-activity:${normalizedAddress}:${DEFAULT_TRADER_ACTIVITY_LIMIT}`
const cached = this.getCache<TraderActivityRow>(cacheKey)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: we don't restrict the cache size, and we don't have any mechanism of handling memory leaks per cache? right now the endpoint is public and any request put data to the cache. Is it ok in this case ?

just thinking: we can create some cap per cache f.e. with fifo or something like that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

right now the endpoint is public and any request put data to the cache

We only set the cache if Dune returned rows for that trader. I don't think it can be DDOS-ed.

Comment thread libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts Outdated
Comment thread libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts Outdated
@limitofzero limitofzero self-requested a review May 6, 2026 12:13
@kernelwhisperer kernelwhisperer merged commit a32d5cc into main May 6, 2026
9 checks passed
@kernelwhisperer kernelwhisperer deleted the feat/affiliate-trader-activity-table branch May 6, 2026 12:59
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