Skip to content

feat: b2b affiliate tracking system#12012

Merged
NeOMakinG merged 27 commits intodevelopfrom
swap-tracking
Mar 11, 2026
Merged

feat: b2b affiliate tracking system#12012
NeOMakinG merged 27 commits intodevelopfrom
swap-tracking

Conversation

@NeOMakinG
Copy link
Collaborator

@NeOMakinG NeOMakinG commented Feb 23, 2026

Summary

Implements the complete B2B affiliate tracking pipeline — enabling integrators (widget users, API consumers, web redirect partners) to track swap volumes and affiliate fees via their Arbitrum address.

Epic: ss-aff — B2B Affiliate Tracking System

Architecture

graph TB
    subgraph Clients
        WEB[Web App<br/>app.shapeshift.com]
        WIDGET[Swap Widget<br/>@shapeshiftoss/swap-widget]
        DASH[Affiliate Dashboard]
        API_USER[API Integrators]
    end

    subgraph Public API
        PAPI[Public API Server<br/>Express · /v1/*]
        DOCS[Scalar API Docs<br/>/docs]
    end

    subgraph Backend Microservices
        SWAP[Swap Service<br/>NestJS · 46 chains · 18 swappers]
        USER[User Service<br/>NestJS · Referrals]
        NOTIF[Notifications Service<br/>NestJS · Push + WS]
    end

    subgraph Storage
        DB_SWAP[(swap_service DB)]
        DB_USER[(user_service DB)]
        DB_NOTIF[(notifications DB)]
    end

    subgraph External
        SWAPPERS[DEX Aggregators<br/>THORChain · 0x · Jupiter<br/>Relay · Portals · Chainflip<br/>CoW · Bebop · AVNU · ...]
        CHAINS[Blockchain Nodes<br/>Unchained APIs · RPC]
    end

    WEB & WIDGET & DASH & API_USER -->|HTTPS| PAPI
    PAPI -->|/swaps/*| SWAP
    PAPI -->|quotes + rates| SWAPPERS
    SWAP -->|referral lookup| USER
    SWAP -->|status alerts| NOTIF
    SWAP --> DB_SWAP
    USER --> DB_USER
    NOTIF --> DB_NOTIF
    SWAP -->|on-chain verify| CHAINS
    NOTIF -->|device tokens| USER
Loading

Flow: Client → Public API → gets quote from DEX aggregators → client signs + broadcasts → client calls GET /v1/swap/status?quoteId=...&txHash=0x... → Public API creates swap record in Swap Service → Swap Service polls chain, verifies affiliate on-chain → affiliate fees tracked.

Changes

Public API (packages/public-api/)

  • Swap status endpoint with automatic txHash binding (no separate register call needed)
  • Affiliate stats endpoint (GET /v1/affiliate/stats?address=...)
  • Rate limiting per affiliate address
  • OpenAPI docs via Scalar with full Widget SDK + REST API guide
  • Removed /v1/swap/register — status endpoint handles txHash binding on first call
  • Production default for SWAP_SERVICE_BASE_URLdev-api.swap-service.shapeshift.com

Swap Widget (packages/swap-widget/)

  • Passes affiliate=0xAddress in redirect URLs to app.shapeshift.com/trade
  • Threads affiliateAddress prop through component hierarchy

Web App (src/)

  • Reads ?affiliate=0xAddr from URL params
  • Persists affiliate address in localStorage for session attribution
  • Passes affiliate address through swap flow to backend

Affiliate Dashboard (packages/affiliate-dashboard/)

  • Standalone React app showing affiliate stats per address
  • Period selector (monthly periods starting on the 5th)
  • ShapeShift logo + dark theme
  • Vite proxy to public-api for local dev

Package Bumps

  • Bumped 8 @shapeshiftoss/* packages to support new affiliate fields

Companion PR

Backend: shapeshift/microservices#19 — Swap Service with affiliate verification, 46 chain adapters, polling, and comprehensive test suite.

How to test:

  • Run the docker file env of the microservices
  • Update web to consume the services (uncomment the env urls for the microservices and comment the others)
  • Launch the widget as well
  • Launch the public-api
  • Try some swaps using the widget (evm, solana, btc...) make sure there is an affiliate address setup in the widget
  • Launch the referral dashboard from web and notice the referral fees shared are same (10bps if done through shapeshift referred or X BPS depending on the widget setup)
  • Try a swap the widget doesnt support, you should be redirected to shapeshift with a referral address and amounts/assets prefilled, execute the swap, notice the dashboard show the correct amounts

Summary by CodeRabbit

  • New Features

    • Added Affiliate Dashboard app and automatic affiliate address tracking from URL/local storage
    • New swap status and affiliate stats endpoints
  • Enhancements

    • Affiliate fee calculations added across swap providers
    • Swap widget accepts affiliateAddress and affiliateBps; affiliate-aware flows/redirects
    • API docs expanded with new endpoints and integration guidance
  • Styling

    • Darkened light-theme borders/text for improved contrast; improved mobile responsiveness

Implement POST /v1/swap/register and GET /v1/swap/status for affiliate
swap tracking. QuoteStore (in-memory Map with dual TTL) stores quotes
after generation and binds txHash on registration.

- QuoteStore: 15min quote TTL, 60min post-tx TTL, automatic sweep
- Register: validates quoteId, chainId, affiliate, duplicate txHash
- Status: returns swap state with affiliate attribution data
- Rate limiting: 10 req/s per affiliate on register endpoint
- Abuse vectors: 404 fabricated, 409 replay, 403 cross-affiliate, 400 chain mismatch
- OpenAPI docs updated with new endpoints
…n tracking, and security hardening

- Public API: configurable BPS via X-Affiliate-Bps header, default 10 BPS base fee
- Public API: swap lifecycle (quote → status+txHash → swap-service registration)
- Public API: affiliate stats proxy endpoint
- Widget: affiliateBps prop, redirect URL affiliate param appended correctly
- Web app: sends affiliateBps, affiliateAddress, origin to swap-service
- Swapper: affiliate fee asset tracking per swapper strategy
- Dashboard: standalone Vite+React affiliate earnings dashboard
- Security: swap creation moved from quote-time to status-time (prevents phantom inflation)
- Lint: all errors resolved in public-api and dashboard packages
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds affiliate tracking, fee calculation, and reporting across frontend, widget, swappers, and public API: new affiliate-dashboard app, affiliate stats and status endpoints, in-memory quote store, affiliate fee utility, middleware and configuration updates, and hooks to persist affiliate addresses.

Changes

Cohort / File(s) Summary
Affiliate Dashboard (new package)
packages/affiliate-dashboard/index.html, packages/affiliate-dashboard/package.json, packages/affiliate-dashboard/src/main.tsx, packages/affiliate-dashboard/src/App.tsx, packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts, packages/affiliate-dashboard/vite.config.ts, packages/affiliate-dashboard/tsconfig*.json
Adds a React + Vite package providing an affiliate stats UI and a hook to fetch /v1/affiliate/stats.
Affiliate tracking hook & exports
src/hooks/useAffiliateTracking/useAffiliateTracking.ts, src/hooks/useAffiliateTracking/index.ts
New SSR-safe hook to read affiliate from URL/localStorage with 30-day TTL; re-exports storage key and helper.
Public API: endpoints, docs, quote store
packages/public-api/src/routes/affiliate.ts, packages/public-api/src/routes/status.ts, packages/public-api/src/index.ts, packages/public-api/src/docs/openapi.ts, packages/public-api/src/lib/quoteStore.ts, packages/public-api/src/routes/docs.ts
Adds GET /v1/affiliate/stats and GET /v1/swap/status, OpenAPI additions, and an in-memory QuoteStore with TTLs, eviction, txHash indexing, and periodic sweeps.
Middleware & config changes
packages/public-api/src/middleware/auth.ts, packages/public-api/src/middleware/rateLimit.ts, packages/public-api/src/config.ts, packages/public-api/src/types.ts
Makes affiliateAddress optional, adds affiliateBps header validation, new rate limiters, DEFAULT_AFFILIATE_BPS change, and SWAP_SERVICE_BASE_URL logic for env/fallback.
Quote/rate/quote lifecycle wiring
packages/public-api/src/routes/quote.ts, packages/public-api/src/routes/rates.ts, src/lib/tradeExecution.ts, src/components/MultiHopTrade/hooks/*, src/pages/Trade/tabs/TradeTab.tsx
Persists affiliateAddress/BPS on quotes, sources affiliateBps from request headers with fallback, threads affiliateAddress through trade input generation and swap payloads, and invokes affiliate tracking on trade page.
Swap-widget: API, props, config, types
packages/swap-widget/src/api/client.ts, packages/swap-widget/src/types/index.ts, packages/swap-widget/src/components/SwapWidget.tsx, packages/swap-widget/src/config/*.ts, packages/swap-widget/vite.config.ts, packages/swap-widget/tsconfig.build.json, packages/swap-widget/package.json
Adds affiliateBps to ApiClientConfig and affiliateAddress/affiliateBps plumbing through widget; integrates Wagmi provider and standalone Wagmi config; build/config updates and package metadata changes.
Swap-widget: hooks, utils, UI
packages/swap-widget/src/hooks/*, packages/swap-widget/src/utils/redirect.ts, packages/swap-widget/src/components/SwapWidget.css, packages/swap-widget/src/demo/App.css
Type annotations for query hooks, thread affiliateAddress into handlers, add buildShapeShiftTradeUrl supporting affiliate param, and adjust UI CSS tokens/responsive demo layout.
Swapper infra: affiliate fee utility & integrations
packages/swapper/src/swappers/utils/affiliateFee.ts, `packages/swapper/src/swappers/**/...(getTradeQuote
getTradeRate
Frontend trade hooks & inputs
src/components/MultiHopTrade/hooks/getTradeQuoteOrRateInput.ts, useGetTradeQuotes.tsx, useGetTradeRateInput.ts
Adds affiliateAddress to trade input types, includes affiliateAddress in query keys and memo deps, and obtains affiliateAddress from useAffiliateTracking.
Minor version bumps
packages/chain-adapters/package.json, packages/errors/package.json, packages/types/package.json, packages/unchained-client/package.json
Package version increments and swap-widget package metadata/version update.

Sequence Diagram(s)

sequenceDiagram
    participant User as Browser/User
    participant Dashboard as Affiliate Dashboard
    participant API as Public API (/v1)
    participant SwapSvc as Swap Service
    participant QuoteStore as QuoteStore
    participant Storage as localStorage

    User->>Dashboard: Enter affiliate address & period
    Dashboard->>Dashboard: Validate address
    Dashboard->>API: GET /v1/affiliate/stats (address,start,end)
    API->>SwapSvc: GET /swaps/affiliate-fees/{address}
    SwapSvc-->>API: BackendAffiliateStats
    API-->>Dashboard: AffiliateStatsResponse
    Dashboard->>User: Display formatted stats

    rect rgba(200,150,100,0.5)
    User->>Dashboard: Visit with ?affiliate=0x...
    Dashboard->>Storage: useAffiliateTracking() read/write (30d TTL)
    end

    rect rgba(100,150,200,0.5)
    Client->>API: POST /v1/swap/quote (with affiliateAddress/affiliateBps)
    API->>QuoteStore: store.set(quoteId, StoredQuote with affiliate metadata)
    Client->>API: GET /v1/swap/status?quoteId=&txHash=
    API->>QuoteStore: get(quoteId)
    API->>QuoteStore: TOCTOU guard & possibly set txHash
    API->>SwapSvc: POST /swaps (include affiliate metadata) / GET status
    SwapSvc-->>API: swap creation/status
    API-->>Client: status response (mapped + affiliate fields)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped a path through headers and stores,

tucked addresses in cookies and tiny drawers,
fees now dance in every swapper's song,
dashboards hum the totals all day long —
a twitch of whiskers, and the tracking's strong.

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: b2b affiliate tracking system' clearly and concisely summarizes the main change — implementation of a B2B affiliate tracking system across multiple packages and services.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch swap-tracking

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.

@NeOMakinG NeOMakinG marked this pull request as ready for review February 26, 2026 22:37
@NeOMakinG NeOMakinG requested a review from a team as a code owner February 26, 2026 22:37
Copy link
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: 12

Caution

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

⚠️ Outside diff range comments (4)
packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts (1)

107-118: ⚠️ Potential issue | 🟠 Major

AVNU affiliate fee should use 'buy_asset' strategy to match protocolFees representation in the same step.

Lines 107–118 calculate protocolFees on the buy-side (from buyAmountAfterFeesCryptoBaseUnit using buyAsset), but lines 146–153 use strategy: 'sell_asset' for affiliateFee, which calculates the fee on the sell-side using sellAmountCryptoBaseUnit and sellAsset. This creates conflicting fee metadata within a single trade step, where both feeData.protocolFees and affiliateFee should represent the same fee consistently.

All other swappers (Across, Sunio, ZRX, Stonfi) use 'buy_asset' strategy. Align AVNU with this pattern to ensure fee consistency.

Suggested fix
           affiliateFee: buildAffiliateFee({
-            strategy: 'sell_asset',
+            strategy: 'buy_asset',
             affiliateBps,
             sellAsset,
             buyAsset,
             sellAmountCryptoBaseUnit: sellAmount,
             buyAmountCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit,
           }),

Also applies to: getTradeQuote.ts lines 180–187

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts` around
lines 107 - 118, The protocolFees calculation currently builds fees on the buy
side using buyAmountAfterFeesCryptoBaseUnit and buyAsset, but affiliateFee is
constructed with strategy: 'sell_asset' using sellAmountCryptoBaseUnit and
sellAsset; change affiliateFee to use the 'buy_asset' strategy and compute its
amount from buyAmountAfterFeesCryptoBaseUnit and buyAsset so both fee
representations match. Update the affiliateFee construction in getTradeRate.ts
(the affiliateFee object where strategy is set) to use strategy: 'buy_asset' and
the same bn(...) computation as protocolFees; make the same change in
getTradeQuote.ts for its affiliateFee to ensure consistency across both files.
packages/swap-widget/src/hooks/useSwapRates.ts (1)

31-31: ⚠️ Potential issue | 🟠 Major

Add affiliate dimensions to the rates cache key.

The useSwapRates hook cache key does not include affiliate dimensions. Since affiliate headers (x-affiliate-address, x-affiliate-bps) affect API responses but are not part of the query key, different affiliate contexts will incorrectly share cached rates. The same issue exists in useSwapQuote. Add affiliateAddress and affiliateBps to both hooks' cache keys, or ensure they're passed as hook parameters rather than only in the apiClient configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swap-widget/src/hooks/useSwapRates.ts` at line 31, The cache key for
useSwapRates (queryKey: ['swapRates', sellAssetId, buyAssetId,
sellAmountCryptoBaseUnit, allowedSwapperNames]) omits affiliate dimensions so
responses returned under different affiliate headers can be incorrectly shared;
update useSwapRates (and mirror the same change in useSwapQuote) to include
affiliateAddress and affiliateBps in the queryKey array (e.g., add
affiliateAddress and affiliateBps alongside sellAssetId, buyAssetId, etc.) or
alternatively accept affiliateAddress and affiliateBps as explicit hook
parameters and use those values in the queryKey so cached results are segregated
by affiliate context.
src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts (1)

130-156: ⚠️ Potential issue | 🟠 Major

Rate-input cache key is missing the affiliate dimension.

tradeInputQueryParams includes affiliateAddress (line 111) with it in the dependency array (line 116), but tradeInputQueryKey (lines 130–156) omits it entirely. This causes the query to reuse cached results when the affiliate address changes, since React Query's cache is keyed by tradeInputQueryKey (line 160).

🧩 Suggested fix
   const tradeInputQueryKey = useMemo(
     () => ({
       buyAsset,
       sellAmountCryptoPrecision,
       sellAsset,
       userSlippageTolerancePercentageDecimal,
+      affiliateAddress,
       // TODO(gomes): all the below are what's causing trade input to refentially invalidate on wallet connect
       // We will need to find a way to have our cake and eat it, by ensuring we get bip44 and other addy-related data to
       // referentially invalidate, while ensuring the *initial* connection of a wallet when quotes were gotten without one, doesn't invalidate anything
       sellAccountMetadata,
       receiveAccountMetadata,
       sellAccountId,
       isBuyAssetChainSupported,
       receiveAddress,
     }),
     [
+      affiliateAddress,
       buyAsset,
       isBuyAssetChainSupported,
       receiveAccountMetadata,
       receiveAddress,
       sellAccountId,
       sellAccountMetadata,
       sellAmountCryptoPrecision,
       sellAsset,
       userSlippageTolerancePercentageDecimal,
     ],
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts` around lines 130
- 156, tradeInputQueryKey is missing the affiliate dimension, causing cached
rate inputs to be reused across affiliate changes; update the useMemo that
builds tradeInputQueryKey to include affiliateAddress in the returned object and
add affiliateAddress to its dependency array so the memo and the React Query key
invalidate when affiliateAddress changes (tradeInputQueryParams already contains
affiliateAddress, so keep them consistent).
src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx (1)

236-255: ⚠️ Potential issue | 🟠 Major

Add affiliateAddress to the useQuery cache key.

affiliateAddress affects the query result (passed to getTradeQuoteOrRateInput) and is already in the queryFnOrSkip dependency array, but it's missing from the queryKey object. When the affiliate changes, the query function is recreated but the cache identity remains the same, which can cause stale cached results to be served.

Suggested fix
   const { data: tradeQuoteInput } = useQuery({
     queryKey: [
       'getTradeQuoteInput',
       {
         buyAsset,
         dispatch,
         receiveAddress,
         sellAccountMetadata,
         sellAmountCryptoPrecision,
         sellAsset,
         receiveAccountMetadata,
         userSlippageTolerancePercentageDecimal,
         sellAssetUsdRate,
         sellAccountId,
         isBuyAssetChainSupported,
         hopExecutionMetadata,
         activeTrade,
+        affiliateAddress,
       },
     ],
     queryFn: queryFnOrSkip,
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx`
around lines 236 - 255, The cache key for the useQuery call in useGetTradeQuotes
(the queryKey object passed into useQuery) is missing affiliateAddress, causing
stale results when affiliate changes; update the queryKey to include
affiliateAddress alongside buyAsset, sellAsset, sellAmountCryptoPrecision,
receiveAddress, sellAccountId, hopExecutionMetadata, activeTrade, etc., so the
key reflects the same dependencies used to build queryFnOrSkip and
getTradeQuoteOrRateInput and triggers a fresh fetch when affiliateAddress
changes.
🧹 Nitpick comments (5)
packages/swap-widget/vite.config.ts (1)

7-13: Remove newly added inline comments in this config.

These comments are not required for functionality and conflict with the repo rule that disallows adding code comments unless explicitly requested.

As per coding guidelines: "Never add code comments unless explicitly requested."

Also applies to: 46-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swap-widget/vite.config.ts` around lines 7 - 13, Remove the newly
added inline comment block starting with "Externals for the library build." and
any other added comments in this file (including the comment at the other noted
location containing the same style) so the file contains only code per repo
rules; locate the top-of-file comment and the secondary comment and delete them
(they are purely explanatory, not functional) ensuring the remaining
exports/logic in vite.config.ts (the externals-handling function and related
code) are unchanged.
packages/swap-widget/package.json (1)

55-56: Constrain React peer range to tested majors.

Lines 55-56 use >=18.0.0, which also permits future React majors that may be untested. Consider pinning to supported majors (e.g., ^18 || ^19) to avoid accidental incompatible installs.

🔧 Proposed fix
-    "react": ">=18.0.0",
-    "react-dom": ">=18.0.0",
+    "react": "^18.0.0 || ^19.0.0",
+    "react-dom": "^18.0.0 || ^19.0.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swap-widget/package.json` around lines 55 - 56, The peerDependencies
for "react" and "react-dom" currently use open-ended ranges (">=18.0.0");
restrict these to the tested major versions by replacing those ranges with
explicit supported-major ranges (for example use "^18 || ^19" or whatever major
sets you have validated) in packages/swap-widget package.json so installs won't
pull untested future major React releases; update both the "react" and
"react-dom" entries to the chosen constrained ranges and bump the package
version/lockfile if required.
packages/swapper/src/swappers/utils/affiliateFee.ts (1)

7-7: Use a string enum for AffiliateFeeStrategy to match project TS constant conventions.

Please switch this new strategy constant type from a string-union to a string enum for consistency with the repository rule set.

As per coding guidelines: “ALWAYS use enums for constants in TypeScript” and “ALWAYS use string enums for better debugging in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swapper/src/swappers/utils/affiliateFee.ts` at line 7, Replace the
string-union type AffiliateFeeStrategy with a exported string enum named
AffiliateFeeStrategy and map each current literal to a descriptive enum member
(e.g., BUY_ASSET = 'buy_asset', SELL_ASSET = 'sell_asset', FIXED_ASSET =
'fixed_asset'); update any existing references to use the enum
(AffiliateFeeStrategy.BUY_ASSET, etc.) so code and imports continue to compile
and follow the project's string-enum convention.
packages/swap-widget/src/config/wagmi.ts (1)

14-26: Type safety reduced for SupportedChainId.

Changing SupportedChainId from a union of literal chain IDs to number removes compile-time validation of chain IDs. This allows any number to pass type checks where a supported chain ID is expected.

If this is intentional for AppKit compatibility, consider adding a runtime validation guard or documenting this tradeoff.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swap-widget/src/config/wagmi.ts` around lines 14 - 26,
SUPPORTED_CHAINS is a fixed readonly array, so changing SupportedChainId to
number loses compile-time validation; update SupportedChainId to derive a union
of the actual chain id literals from SUPPORTED_CHAINS (for example: export type
SupportedChainId = typeof SUPPORTED_CHAINS[number]['id']) so only supported IDs
are allowed by the type system, and if AppKit compatibility requires a plain
number keep the number alias but add a runtime guard function (e.g.,
isSupportedChainId(id): id is SupportedChainId) that checks against
SUPPORTED_CHAINS to validate values at runtime.
packages/public-api/src/middleware/rateLimit.ts (1)

14-24: Consider a shared backend for production rate-limiting.

This in-memory map is process-local. In multi-instance deployments, effective limits scale with replica count. A shared store (e.g., Redis) will keep limits consistent across nodes.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts`:
- Around line 38-85: The fetchStats function can let stale async responses
overwrite newer state; fix it by introducing a per-request guard (e.g., a
requestId counter ref or an AbortController) inside the useAffiliateStats hook:
increment a requestId (or abort previous controller) at the start of fetchStats,
capture the current id (or signal) before awaiting the fetch, and only call
setStats, setError or setIsLoading if the captured id still matches the latest
id (or the fetch was not aborted). Update symbols: modify fetchStats in
useAffiliateStats to use a requestIdRef (or controllerRef) and check it before
setStats/setError in the try/catch/finally blocks so older responses cannot
overwrite newer state.

In `@packages/public-api/src/docs/openapi.ts`:
- Around line 431-446: The OpenAPI schema SwapStatusResponseSchema (registered
as 'SwapStatusResponse') is missing the buyTxHash and isAffiliateVerified fields
returned by the /v1/swap/status handler; update the z.object passed to
registry.register in SwapStatusResponseSchema to include buyTxHash (a
string.optional() representing the buy transaction hash) and isAffiliateVerified
(a boolean.optional() representing affiliate verification), keeping naming
exactly buyTxHash and isAffiliateVerified and preserving existing
optional/required semantics so the schema matches the handler response.

In `@packages/public-api/src/lib/quoteStore.ts`:
- Line 45: The static unsubmitted-quote TTL constant QUOTE_TTL_MS is set to 2
minutes but should match the documented 15-minute validity window; update the
value of static readonly QUOTE_TTL_MS (in quoteStore.ts / QuoteStore) to 15 * 60
* 1000 (15 minutes) and adjust any related tests or comments that assume the
previous 2-minute value so unsubmitted quotes do not expire prematurely.

In `@packages/public-api/src/middleware/rateLimit.ts`:
- Around line 25-27: The limiter key currently uses the raw affiliate address
(getKey) which allows easy bypass; change getKey to build a hardened key by
normalizing the affiliate address (trim and toLowerCase, optionally strip 0x
prefix) and concatenating a stable network identifier (use
req.affiliateInfo?.network if available, else fall back to a request header like
req.headers['x-network'] or 'unknown-net'), e.g.
"<network>:<normalizedAddress>"; only if no affiliate info exists fall back to
using req.ip or req.socket.remoteAddress, and always return a single
deterministic string to avoid bucket-splitting (refer to getKey and the uses of
req.affiliateInfo?.affiliateAddress, req.affiliateInfo?.network, req.ip, and
req.socket.remoteAddress).

In `@packages/public-api/src/routes/affiliate.ts`:
- Around line 15-17: The schema currently allows startDate and endDate
individually but doesn't prevent startDate being after endDate; update the Zod
schema that defines startDate and endDate (the object with keys startDate and
endDate in packages/public-api/src/routes/affiliate.ts) to add a cross-field
validation (use superRefine or refine on the parsed object) that checks when
both dates are present that new Date(startDate) <= new Date(endDate), and return
a clear validation error on the appropriate path (e.g., addIssue for "startDate"
or "endDate") so invalid ranges are rejected early with a helpful message.
- Around line 65-67: The fetch to swap-service using backendUrl and assigning
backendResponse is missing a timeout; wrap the request with an AbortController
(use the same timeout constant GAS_FEES_TIMEOUT_MS from swapperDeps.ts), call
setTimeout to controller.abort() after the timeout, pass controller.signal to
fetch(backendUrl.toString(), { signal }), and clear the timeout when the fetch
resolves; also handle the abort case in the catch (treat AbortError or
DOMException name 'AbortError' as a timeout) so the handler returns an
appropriate timeout error instead of hanging.

In `@packages/public-api/src/routes/docs.ts`:
- Line 11: Remove the newly added inline comment "// Serve favicon" from
packages/public-api/src/routes/docs.ts; locate that exact comment in the file
(it was added near the top of the route definitions) and delete the line so the
file conforms to the repository guideline that forbids adding non-requested
comments.

In `@packages/public-api/src/routes/status.ts`:
- Around line 33-41: getSwapStatus currently returns stored quotes without
verifying affiliate ownership; after retrieving storedQuote via
quoteStore.get(quoteId) check the caller's affiliate context (the
request-affiliate value your code uses, e.g., req.affiliateAddress or
res.locals.affiliate) against storedQuote.affiliateAddress and if they differ
deny access (return 404 or 403 with the same ErrorResponse shape). Update the
lookup path in getSwapStatus to perform this equality check before sending the
quote, using storedQuote and storedQuote.affiliateAddress to locate the data and
the request-affiliate variable your app populates as the authority for the
caller.
- Around line 54-75: The TOCTOU happens because you mutate storedQuote after an
earlier check on current; instead perform an atomic re-check before writing:
re-read the latest quote from quoteStore (e.g., call quoteStore.get(quoteId)
into latest) immediately before assigning storedQuote.txHash/registeredAt/status
and, if latest?.txHash exists and latest.txHash !== txHash, return the existing
response (like the earlier branch); if latest.txHash === txHash also return
without resetting metadata; only then set quoteStore.set(quoteId, updatedQuote).
This implements a compare-and-swap style guard around storedQuote/quoteStore to
prevent overwriting when another request already bound the txHash.

In `@packages/swap-widget/src/config/standaloneWagmi.ts`:
- Around line 1-6: Remove the top-level docblock comment at the top of the file
that describes "Standalone wagmi configuration for read-only RPC access" (the
multi-line /** ... */ block) so the file no longer contains a top-level block
comment; leave any inline or function-level comments intact and ensure
exports/identifiers such as SwapWidgetWithExternalWallet or standalone wagmi
configuration code remain unchanged.

In `@packages/swapper/src/swappers/AcrossSwapper/utils/getTrade.ts`:
- Around line 426-433: affiliateFee is always computed via buildAffiliateFee
even when app/app-fee was intentionally disabled earlier; change the assignment
so affiliateFee is only set when the same flag used to enable app fees in this
function (reuse the existing symbol used earlier, e.g., appFee or appFeeEnabled)
is truthy—otherwise set affiliateFee to undefined (or zero if callers expect a
number). Concretely, wrap or conditionalize the buildAffiliateFee call (the
affiliateFee property) behind that existing app-fee eligibility check so
buildAffiliateFee(...) is only invoked when app fees are enabled.

In
`@packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts`:
- Around line 190-197: The affiliateFee built in getCowSwapTradeQuote is missing
the isEstimate flag; update the buildAffiliateFee call (the affiliateFee
assignment inside getCowSwapTradeQuote) to include isEstimate: true so the
returned AffiliateFee object matches other swappers (e.g., PortalsSwapper,
BebopSwapper) and signals this is an estimated fee.

---

Outside diff comments:
In `@packages/swap-widget/src/hooks/useSwapRates.ts`:
- Line 31: The cache key for useSwapRates (queryKey: ['swapRates', sellAssetId,
buyAssetId, sellAmountCryptoBaseUnit, allowedSwapperNames]) omits affiliate
dimensions so responses returned under different affiliate headers can be
incorrectly shared; update useSwapRates (and mirror the same change in
useSwapQuote) to include affiliateAddress and affiliateBps in the queryKey array
(e.g., add affiliateAddress and affiliateBps alongside sellAssetId, buyAssetId,
etc.) or alternatively accept affiliateAddress and affiliateBps as explicit hook
parameters and use those values in the queryKey so cached results are segregated
by affiliate context.

In `@packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts`:
- Around line 107-118: The protocolFees calculation currently builds fees on the
buy side using buyAmountAfterFeesCryptoBaseUnit and buyAsset, but affiliateFee
is constructed with strategy: 'sell_asset' using sellAmountCryptoBaseUnit and
sellAsset; change affiliateFee to use the 'buy_asset' strategy and compute its
amount from buyAmountAfterFeesCryptoBaseUnit and buyAsset so both fee
representations match. Update the affiliateFee construction in getTradeRate.ts
(the affiliateFee object where strategy is set) to use strategy: 'buy_asset' and
the same bn(...) computation as protocolFees; make the same change in
getTradeQuote.ts for its affiliateFee to ensure consistency across both files.

In `@src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx`:
- Around line 236-255: The cache key for the useQuery call in useGetTradeQuotes
(the queryKey object passed into useQuery) is missing affiliateAddress, causing
stale results when affiliate changes; update the queryKey to include
affiliateAddress alongside buyAsset, sellAsset, sellAmountCryptoPrecision,
receiveAddress, sellAccountId, hopExecutionMetadata, activeTrade, etc., so the
key reflects the same dependencies used to build queryFnOrSkip and
getTradeQuoteOrRateInput and triggers a fresh fetch when affiliateAddress
changes.

In `@src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts`:
- Around line 130-156: tradeInputQueryKey is missing the affiliate dimension,
causing cached rate inputs to be reused across affiliate changes; update the
useMemo that builds tradeInputQueryKey to include affiliateAddress in the
returned object and add affiliateAddress to its dependency array so the memo and
the React Query key invalidate when affiliateAddress changes
(tradeInputQueryParams already contains affiliateAddress, so keep them
consistent).

---

Nitpick comments:
In `@packages/swap-widget/package.json`:
- Around line 55-56: The peerDependencies for "react" and "react-dom" currently
use open-ended ranges (">=18.0.0"); restrict these to the tested major versions
by replacing those ranges with explicit supported-major ranges (for example use
"^18 || ^19" or whatever major sets you have validated) in packages/swap-widget
package.json so installs won't pull untested future major React releases; update
both the "react" and "react-dom" entries to the chosen constrained ranges and
bump the package version/lockfile if required.

In `@packages/swap-widget/src/config/wagmi.ts`:
- Around line 14-26: SUPPORTED_CHAINS is a fixed readonly array, so changing
SupportedChainId to number loses compile-time validation; update
SupportedChainId to derive a union of the actual chain id literals from
SUPPORTED_CHAINS (for example: export type SupportedChainId = typeof
SUPPORTED_CHAINS[number]['id']) so only supported IDs are allowed by the type
system, and if AppKit compatibility requires a plain number keep the number
alias but add a runtime guard function (e.g., isSupportedChainId(id): id is
SupportedChainId) that checks against SUPPORTED_CHAINS to validate values at
runtime.

In `@packages/swap-widget/vite.config.ts`:
- Around line 7-13: Remove the newly added inline comment block starting with
"Externals for the library build." and any other added comments in this file
(including the comment at the other noted location containing the same style) so
the file contains only code per repo rules; locate the top-of-file comment and
the secondary comment and delete them (they are purely explanatory, not
functional) ensuring the remaining exports/logic in vite.config.ts (the
externals-handling function and related code) are unchanged.

In `@packages/swapper/src/swappers/utils/affiliateFee.ts`:
- Line 7: Replace the string-union type AffiliateFeeStrategy with a exported
string enum named AffiliateFeeStrategy and map each current literal to a
descriptive enum member (e.g., BUY_ASSET = 'buy_asset', SELL_ASSET =
'sell_asset', FIXED_ASSET = 'fixed_asset'); update any existing references to
use the enum (AffiliateFeeStrategy.BUY_ASSET, etc.) so code and imports continue
to compile and follow the project's string-enum convention.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6999bfa and e0075ab.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (80)
  • .env.development
  • packages/affiliate-dashboard/index.html
  • packages/affiliate-dashboard/package.json
  • packages/affiliate-dashboard/src/App.tsx
  • packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts
  • packages/affiliate-dashboard/src/main.tsx
  • packages/affiliate-dashboard/tsconfig.json
  • packages/affiliate-dashboard/tsconfig.node.json
  • packages/affiliate-dashboard/vite.config.ts
  • packages/chain-adapters/package.json
  • packages/contracts/package.json
  • packages/errors/package.json
  • packages/public-api/src/config.ts
  • packages/public-api/src/docs/openapi.ts
  • packages/public-api/src/index.ts
  • packages/public-api/src/lib/quoteStore.ts
  • packages/public-api/src/middleware/auth.ts
  • packages/public-api/src/middleware/rateLimit.ts
  • packages/public-api/src/routes/affiliate.ts
  • packages/public-api/src/routes/docs.ts
  • packages/public-api/src/routes/quote.ts
  • packages/public-api/src/routes/rates.ts
  • packages/public-api/src/routes/status.ts
  • packages/public-api/src/types.ts
  • packages/swap-widget/package.json
  • packages/swap-widget/src/api/client.ts
  • packages/swap-widget/src/components/SwapWidget.css
  • packages/swap-widget/src/components/SwapWidget.tsx
  • packages/swap-widget/src/config/appkit.ts
  • packages/swap-widget/src/config/standaloneWagmi.ts
  • packages/swap-widget/src/config/wagmi.ts
  • packages/swap-widget/src/demo/App.css
  • packages/swap-widget/src/hooks/useAssets.ts
  • packages/swap-widget/src/hooks/useBalances.ts
  • packages/swap-widget/src/hooks/useMarketData.ts
  • packages/swap-widget/src/hooks/useSwapDisplayValues.ts
  • packages/swap-widget/src/hooks/useSwapHandlers.ts
  • packages/swap-widget/src/hooks/useSwapQuote.ts
  • packages/swap-widget/src/hooks/useSwapRates.ts
  • packages/swap-widget/src/types/index.ts
  • packages/swap-widget/src/utils/redirect.ts
  • packages/swap-widget/tsconfig.build.json
  • packages/swap-widget/vite.config.ts
  • packages/swapper/package.json
  • packages/swapper/src/swappers/AcrossSwapper/utils/getTrade.ts
  • packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/BebopSwapper/getBebopTradeQuote/getBebopTradeQuote.ts
  • packages/swapper/src/swappers/BebopSwapper/getBebopTradeRate/getBebopTradeRate.ts
  • packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/ButterSwap/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/CetusSwapper/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/ChainflipSwapper/utils/getQuoteOrRate.ts
  • packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts
  • packages/swapper/src/swappers/CowSwapper/getCowSwapTradeRate/getCowSwapTradeRate.ts
  • packages/swapper/src/swappers/JupiterSwapper/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/JupiterSwapper/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/PortalsSwapper/getPortalsTradeQuote/getPortalsTradeQuote.ts
  • packages/swapper/src/swappers/RelaySwapper/utils/getTrade.ts
  • packages/swapper/src/swappers/StonfiSwapper/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/StonfiSwapper/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/SunioSwapper/utils/getQuoteOrRate.ts
  • packages/swapper/src/swappers/ZrxSwapper/getZrxTradeQuote/getZrxTradeQuote.ts
  • packages/swapper/src/swappers/ZrxSwapper/getZrxTradeRate/getZrxTradeRate.ts
  • packages/swapper/src/swappers/utils/affiliateFee.ts
  • packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts
  • packages/swapper/src/types.ts
  • packages/types/package.json
  • packages/unchained-client/package.json
  • packages/utils/package.json
  • src/components/MultiHopTrade/hooks/useGetTradeQuotes/getTradeQuoteOrRateInput.ts
  • src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx
  • src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts
  • src/hooks/useAffiliateTracking/index.ts
  • src/hooks/useAffiliateTracking/useAffiliateTracking.ts
  • src/lib/tradeExecution.ts
  • src/pages/Trade/tabs/TradeTab.tsx

Copy link
Collaborator Author

@NeOMakinG NeOMakinG left a comment

Choose a reason for hiding this comment

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

Code Review — PR #12012

Verdict: ⚠️ COMMENT — Needs full review, providing initial assessment

Summary: Complete B2B affiliate tracking pipeline — Public API, Swap Widget updates, Web App integration, and standalone Affiliate Dashboard. 81 files, 3469 additions. This is a major feature requiring careful security and architecture review.

Architecture Assessment

The flow makes sense:

  1. Client gets quote from DEX aggregators via Public API
  2. Client signs + broadcasts
  3. Status endpoint (GET /v1/swap/status?quoteId=...&txHash=0x...) creates swap record
  4. Swap Service polls chain, verifies affiliate on-chain
  5. Affiliate fees tracked and queryable

Key design decisions:

  • No separate register call — status endpoint handles txHash binding on first call (simpler API surface)
  • Rate limiting per affiliate address — good for abuse prevention
  • localStorage persistence — affiliate address persists across sessions via URL param → localStorage
  • Period-based stats — monthly periods starting on the 5th

What I Verified (High-level)

  • All swappers updated to thread affiliateAddress through quote/rate inputs (14+ swappers touched)
  • Public API has auth middleware, rate limiting, OpenAPI docs
  • Widget passes affiliate=0xAddress via redirect URLs
  • Web app reads ?affiliate= from URL params and persists in localStorage
  • Package bumps for @shapeshiftoss/* packages to support new affiliate fields

Areas Needing Deeper Review

  1. Security of affiliate parameter injection: How is the affiliate URL param validated? Arbitrary address injection could redirect fees.
  2. Rate limiting implementation: Need to verify the rate limiter is per-endpoint AND per-affiliate, not just global.
  3. localStorage trust: If affiliate address comes from URL param → localStorage, a malicious link could override a legitimate affiliate's tracking.
  4. API key management: The Public API has auth middleware — how are API keys distributed and rotated?
  5. Affiliate fee calculation: Where exactly are fees computed? On-chain verification vs off-chain trust.

Risk Assessment

Medium-high — New API surface, affiliate fee attribution, localStorage-based session tracking. No CI pipeline run visible (only CodeRabbit). Needs a second reviewer with domain expertise in the affiliate/revenue system.

Browser Test

This PR adds UI-facing changes (URL param handling, affiliate dashboard). Browser testing would be valuable but requires the Public API backend running.

Recommendation

This PR would benefit from a second thorough review given its scope (81 files, new API surface, financial attribution). I've provided architectural observations but a line-by-line review of the Public API auth/rate-limiting and the affiliate fee flow would be prudent.

- Fix quoteStore TTL to match documented 15-minute validity window
- Harden rate limiter key with address normalization and IP composite
- Add trust proxy config for correct IP detection behind reverse proxy
- Add date range validation and fetch timeout to affiliate stats endpoint
- Add missing buyTxHash/isAffiliateVerified to OpenAPI schema
- Add affiliate ownership check and fix TOCTOU guard in swap status
- Add txHash max length validation
- Remove non-functional comments per repo conventions
- Gate Across affiliateFee behind appFee eligibility
- Add isEstimate flag to CowSwap affiliate fee
- Fix AVNU affiliate fee strategy from sell_asset to buy_asset
- Add affiliate dimensions to widget/web app query cache keys
- Fix tradeExecution.ts to use readStoredAffiliate() with TTL check
- Add stale response guard to affiliate dashboard hook
- Add dev URL warning log, constrain React peer range
# Conflicts:
#	packages/public-api/src/docs/openapi.ts
#	packages/public-api/src/index.ts
#	packages/public-api/src/middleware/rateLimit.ts
#	packages/swapper/src/types.ts
#	yarn.lock
Copy link
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 (3)
packages/public-api/src/docs/openapi.ts (2)

494-525: Missing 429 rate limit response in OpenAPI spec.

The /v1/swap/status endpoint uses swapStatusLimiter but the OpenAPI spec doesn't document the 429 response. This inconsistency affects API consumers who rely on the spec for error handling.

📝 Suggested fix
   responses: {
       description: 'Swap status',
       content: {
         'application/json': {
           schema: SwapStatusResponseSchema,
         },
       },
     },
       description: 'Invalid request parameters',
     },
       description: 'Quote not found or expired',
     },
       description: 'Transaction hash mismatch',
     },
+    429: rateLimitResponse,
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/public-api/src/docs/openapi.ts` around lines 494 - 525, The OpenAPI
registration for getSwapStatus (registry.registerPath with operationId
'getSwapStatus') is missing a 429 response for the rate limiter; update the
responses object for the '/v1/swap/status' path to include a 429 entry (e.g.,
description 'Too many requests - rate limit exceeded') and, if appropriate, add
a response body schema (application/json) describing the rate-limit error so
clients know to handle swapStatusLimiter-triggered throttling.

538-565: Missing 429 rate limit response in OpenAPI spec.

The /v1/affiliate/stats endpoint uses affiliateStatsLimiter but the OpenAPI spec doesn't document the 429 response.

📝 Suggested fix
   responses: {
       description: 'Affiliate statistics',
       content: {
         'application/json': {
           schema: AffiliateStatsResponseSchema,
         },
       },
     },
       description: 'Invalid address format',
     },
       description: 'Swap service unavailable',
     },
+    429: rateLimitResponse,
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/public-api/src/docs/openapi.ts` around lines 538 - 565, The OpenAPI
registration for the getAffiliateStats path (registry.registerPath with
operationId 'getAffiliateStats') is missing a 429 response to reflect the
affiliateStatsLimiter; update the responses object for that route to add a 429
entry (e.g., description 'Too many requests') and include the standard
Retry-After header (and optional error schema if you have a common
RateLimitError schema) so the spec matches the runtime rate limiting behavior.
packages/swapper/src/types.ts (1)

201-201: Use a stronger domain type for affiliate address.

Line 201 currently uses string; for an Arbitrum affiliate identifier, prefer Address (or a nominal alias) to prevent invalid values from flowing into swapper inputs.

Proposed change
 type CommonTradeInputBase = {
   sellAsset: Asset
   buyAsset: Asset
   sellAmountIncludingProtocolFeesCryptoBaseUnit: string
   affiliateBps: string
-  affiliateAddress?: string
+  affiliateAddress?: Address
   allowMultiHop: boolean
   slippageTolerancePercentageDecimal?: string
 }
#!/bin/bash
set -euo pipefail

# Verify how affiliateAddress is produced/consumed and whether it is normalized/validated as an EVM address.
rg -n -C3 '\baffiliateAddress\b' -g '**/*.{ts,tsx,js,jsx}'
rg -n -C3 '\b(isAddress|getAddress)\s*\(' -g '**/*.{ts,tsx,js,jsx}'

Expected verification result: callsites passing affiliateAddress should already normalize/validate EVM addresses, making the stronger type safe to adopt.
As per coding guidelines: "**/*.ts: Use Nominal types for domain identifiers (e.g., WalletId, AccountId)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swapper/src/types.ts` at line 201, The affiliateAddress field is
typed as a plain string and should be narrowed to the stronger domain type
(e.g., Address or a Nominal alias) to prevent invalid EVM addresses from
entering swapper inputs; change the type of affiliateAddress in the
SwapInput/related interface(s) to Address (or a new Nominal type like
AffiliateAddress) and update imports/exports accordingly, then run the suggested
grep checks to verify all call sites referencing affiliateAddress (and any
normalization helpers like isAddress/getAddress) already validate/normalize
values so this stronger type is safe to adopt; update any call sites that
construct affiliateAddress to normalize/validate before passing the value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts`:
- Around line 306-313: Replace the recomputed affiliate fee (built from
buyAmountAfterFeesCryptoBaseUnit via buildAffiliateFee) with the
Thornode-provided affiliate fee: take route.quote.fees.affiliate and convert it
to the asset's crypto-base-unit precision (the same conversion used elsewhere in
this file) and assign that value to steps[0].affiliateFee instead of calling
buildAffiliateFee; keep the rest of the step structure unchanged so
steps[0].affiliateFee uses the authoritative route.quote.fees.affiliate value.

In `@src/hooks/useAffiliateTracking/useAffiliateTracking.ts`:
- Around line 31-39: The code returns the stored affiliate address without
validating its format; update the return path (where it currently returns
address) to first call isAddress(address) and if that returns false call
clearAffiliateStorage() and return null (optionally log a clear validation
message), otherwise return the validated address; keep existing expiry check
with isAffiliateExpired(timestamp) and reuse clearAffiliateStorage() on
invalid/expired data.

---

Nitpick comments:
In `@packages/public-api/src/docs/openapi.ts`:
- Around line 494-525: The OpenAPI registration for getSwapStatus
(registry.registerPath with operationId 'getSwapStatus') is missing a 429
response for the rate limiter; update the responses object for the
'/v1/swap/status' path to include a 429 entry (e.g., description 'Too many
requests - rate limit exceeded') and, if appropriate, add a response body schema
(application/json) describing the rate-limit error so clients know to handle
swapStatusLimiter-triggered throttling.
- Around line 538-565: The OpenAPI registration for the getAffiliateStats path
(registry.registerPath with operationId 'getAffiliateStats') is missing a 429
response to reflect the affiliateStatsLimiter; update the responses object for
that route to add a 429 entry (e.g., description 'Too many requests') and
include the standard Retry-After header (and optional error schema if you have a
common RateLimitError schema) so the spec matches the runtime rate limiting
behavior.

In `@packages/swapper/src/types.ts`:
- Line 201: The affiliateAddress field is typed as a plain string and should be
narrowed to the stronger domain type (e.g., Address or a Nominal alias) to
prevent invalid EVM addresses from entering swapper inputs; change the type of
affiliateAddress in the SwapInput/related interface(s) to Address (or a new
Nominal type like AffiliateAddress) and update imports/exports accordingly, then
run the suggested grep checks to verify all call sites referencing
affiliateAddress (and any normalization helpers like isAddress/getAddress)
already validate/normalize values so this stronger type is safe to adopt; update
any call sites that construct affiliateAddress to normalize/validate before
passing the value.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e0075ab and 14949cc.

📒 Files selected for processing (25)
  • .env.development
  • packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts
  • packages/public-api/src/config.ts
  • packages/public-api/src/docs/openapi.ts
  • packages/public-api/src/index.ts
  • packages/public-api/src/lib/quoteStore.ts
  • packages/public-api/src/middleware/rateLimit.ts
  • packages/public-api/src/routes/affiliate.ts
  • packages/public-api/src/routes/docs.ts
  • packages/public-api/src/routes/status.ts
  • packages/swap-widget/package.json
  • packages/swap-widget/src/config/standaloneWagmi.ts
  • packages/swap-widget/src/hooks/useSwapRates.ts
  • packages/swap-widget/vite.config.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getTrade.ts
  • packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeQuote.ts
  • packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeRate.ts
  • packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts
  • packages/swapper/src/thorchain-utils/getL1RateOrQuote.ts
  • packages/swapper/src/types.ts
  • packages/unchained-client/package.json
  • src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx
  • src/components/MultiHopTrade/hooks/useGetTradeRateInput.ts
  • src/hooks/useAffiliateTracking/useAffiliateTracking.ts
  • src/lib/tradeExecution.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx
  • src/lib/tradeExecution.ts
  • packages/swap-widget/src/config/standaloneWagmi.ts
  • packages/swapper/src/swappers/CowSwapper/getCowSwapTradeQuote/getCowSwapTradeQuote.ts
  • packages/swapper/src/swappers/AvnuSwapper/swapperApi/getTradeQuote.ts
  • packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts
  • packages/public-api/src/lib/quoteStore.ts
  • packages/public-api/src/routes/status.ts
  • packages/swapper/src/swappers/AcrossSwapper/utils/getTrade.ts

@NeOMakinG
Copy link
Collaborator Author

🧪 QA Report

CI Status: ❌ FAILING

Failed step: Lint (Static checks)

PR: B2B affiliate tracking system

This is a larger feature PR. Please:

  1. Rebase on latest develop
  2. Fix any lint/type errors
  3. Re-request QA once CI passes

Once CI Passes

Testing checklist:

  • Affiliate tracking parameters passed correctly in swap URLs
  • Tracking events fire with correct affiliate data
  • Dashboard displays affiliate stats correctly

Copy link
Collaborator Author

@NeOMakinG NeOMakinG left a comment

Choose a reason for hiding this comment

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

QABot Code Review 📋

Scope: Reviewed affiliate tracking flow, public-api routes, and storage implementation.

Architecture Assessment:

Good:

  • Clean affiliate stats endpoint with Zod validation (/v1/affiliate/stats)
  • Proper error handling (timeout, 404 → zero values, service errors)
  • useAffiliateTracking hook with 30-day TTL and localStorage persistence
  • In-memory quote store with dual TTL (15min quote validity, 60min execution tracking)
  • Affiliate address validation using isAddress from viem
  • Proper cleanup sweep every 60 seconds
  • Migration path to Redis documented

⚠️ Notes:

  • QuoteStore is in-memory only - will lose data on server restart (acceptable for MVP)
  • MAX_QUOTES = 10000 limit may need monitoring in production
  • Consider adding metrics/logging for affiliate tracking stats

Files Reviewed:

  • packages/public-api/src/routes/affiliate.ts - Stats endpoint
  • packages/public-api/src/lib/quoteStore.ts - Quote storage
  • src/hooks/useAffiliateTracking/useAffiliateTracking.ts - Client-side tracking
  • Swapper fee propagation across 15+ swapper implementations

Recommendation: Ready for manual QA testing of the full flow (widget → redirect → swap → dashboard).

@NeOMakinG
Copy link
Collaborator Author

🔄 Conflicts resolved

Merged develop into swap-tracking:

  • Resolved package.json version conflicts (accepted develop versions)
  • Removed stale yarn.lock (project migrated to pnpm)

PR is now mergeable.

@NeOMakinG NeOMakinG changed the title feat: B2B affiliate tracking system feat: b2b affiliate tracking system Mar 6, 2026
Copy link
Contributor

@gomesalexandre gomesalexandre left a comment

Choose a reason for hiding this comment

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

Deep review - B2B Affiliate Tracking System

Did a thorough code review + agent-browser testing on localhost:3001.

Testing results

All client-side affiliate tracking works correctly:

  • ?affiliate=0x... param captured and stored in localStorage
  • Persists across navigation (no param needed on subsequent visits)
  • First-touch attribution model works - second affiliate address does NOT override a non-expired first one
  • Invalid addresses (non-EVM, garbage strings) correctly rejected via isAddress()
  • XSS payloads (<script>alert(1)</script>) rejected
  • No console errors during any of the above

Overall

Solid v1 with known limitations properly documented (in-memory QuoteStore, Redis migration path). All CodeRabbit findings appear addressed in the fix commits. The buildAffiliateFee utility is clean and the propagation across 16+ swappers is consistent.

A few non-blocking suggestions inline. Nothing that warrants blocking merge imo - these can be follow-ups.

(Could not fully E2E test the public API -> swap-service pipeline since that requires the Docker microservices stack, but the code paths look correct from reading.)

Copy link
Contributor

@gomesalexandre gomesalexandre left a comment

Choose a reason for hiding this comment

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

Approving - solid v1. Inline suggestions below since GitHub can't resolve diff paths on this PR (4000+ changed files from yarn cache):


suggestion - src/hooks/useAffiliateTracking/useAffiliateTracking.ts:38
readStoredAffiliate() trusts whatever is in localStorage without re-validating with isAddress(). If localStorage gets corrupted or manually edited (devtools, extensions, etc.), this would return a garbage string into tradeExecution.ts:215. A defensive isAddress() check on the read path would close the loop:

if (!isAddress(address)) {
  clearAffiliateStorage()
  return null
}
return address

suggestion - packages/public-api/src/routes/status.ts:93,134
The fetch() calls to swap-service have no timeout, unlike affiliate.ts which correctly uses AbortController with a 10s timeout. If swap-service is slow/hung, the handler blocks indefinitely.


q - packages/public-api/src/routes/status.ts:62
The TOCTOU comment says "re-read from store in case a concurrent request already bound a txHash" - but Node.js is single-threaded, and lines 33-63 are all synchronous (no await). The guard is fine as defense-in-depth, but the comment is slightly misleading - it's future-proofing, not protecting against an actual current race.


suggestion - packages/swapper/src/swappers/DebridgeSwapper/utils/getTrade.ts
DebridgeSwapper has its own affiliate fee mechanism (affiliateFeePercent + affiliateFeeRecipient) but doesn't call buildAffiliateFee() or populate the affiliateFee field on its trade step, unlike all other swappers. Fee still gets charged via the API, but step-level affiliateFee will be undefined. Worth a follow-up for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@gomesalexandre gomesalexandre left a comment

Choose a reason for hiding this comment

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

Now with inline comments (yarn cache removal unborked GitHub's diff indexing):

Copy link
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.

Caution

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

⚠️ Outside diff range comments (1)
packages/swap-widget/src/hooks/useSwapRates.ts (1)

33-49: ⚠️ Potential issue | 🟡 Minor

affiliateAddress parameter in queryKey is not used in the actual API call.

The affiliateAddress is included in the queryKey (line 39) but never passed to apiClient.getRates() (lines 45-49). The ApiClient bakes the affiliate address into request headers at creation time, so the parameter in the hook's params object is superfluous for the API call itself.

While including it in the queryKey allows cache separation when different ApiClient instances with different configured affiliates are used, the parameter is misleading since passing it to useSwapRates doesn't actually affect the request—only the ApiClient's configuration does. Consider either removing the parameter from UseSwapRatesParams or documenting that it only affects caching and not the actual API call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/swap-widget/src/hooks/useSwapRates.ts` around lines 33 - 49, The
affiliateAddress included in the queryKey of the useSwapRates hook is misleading
because it is not passed to apiClient.getRates (so it doesn't affect the
request); update the code to either (A) remove affiliateAddress from
UseSwapRatesParams and from the queryKey in useSwapRates so the param is not
accepted or used at all, or (B) keep it only for cache separation but add an
inline comment in useSwapRates and the type UseSwapRatesParams clarifying
"affiliateAddress is used only for cache key separation and not sent to
apiClient.getRates (apiClient config controls headers)". Ensure references are
to the queryKey array in useSwapRates and apiClient.getRates so reviewers can
verify the change.
🧹 Nitpick comments (2)
packages/public-api/src/routes/affiliate.ts (2)

82-99: Minor: Redundant clearTimeout call.

clearTimeout(timeout) is called both in the catch block (line 83) and in the finally block (line 98). While harmless (calling clearTimeout on an already-cleared timer is a no-op), removing line 83 would simplify the code since the finally block handles cleanup for all paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/public-api/src/routes/affiliate.ts` around lines 82 - 99, Remove the
redundant clearTimeout(timeout) from the catch block so the timer cleanup is
only performed once in the finally block; specifically, in the error handling
inside the try/catch around the swap-service request (the catch that checks for
DOMException/AbortError and then logs and responds), delete the
clearTimeout(timeout) call and leave the single clearTimeout(timeout) in the
finally block to handle all paths.

124-135: Consider validating the backend response shape.

Line 127 uses a type assertion (as BackendAffiliateStats) without runtime validation. If the swap-service response shape changes or returns unexpected data, accessing properties like backendData.swapCount could return undefined and propagate silently.

Consider adding a Zod schema to validate the backend response, similar to the request validation pattern:

♻️ Suggested improvement
+const BackendAffiliateStatsSchema = z.object({
+  affiliateAddress: z.string(),
+  swapCount: z.number(),
+  totalSwapVolumeUsd: z.string(),
+  totalFeesCollectedUsd: z.string(),
+  referrerCommissionUsd: z.string(),
+})
+
 // Parse backend response
-let backendData: BackendAffiliateStats
 try {
-  backendData = (await backendResponse.json()) as BackendAffiliateStats
+  const json = await backendResponse.json()
+  const parseResult = BackendAffiliateStatsSchema.safeParse(json)
+  if (!parseResult.success) {
+    console.error('Invalid backend response shape:', parseResult.error.errors)
+    res.status(503).json({
+      error: 'Invalid response from swap service',
+      code: 'INVALID_RESPONSE',
+    } as ErrorResponse)
+    return
+  }
+  backendData = parseResult.data
 } catch (error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/public-api/src/routes/affiliate.ts` around lines 124 - 135, The code
currently asserts the parsed JSON as BackendAffiliateStats (backendData = (await
backendResponse.json()) as BackendAffiliateStats) without runtime validation;
add a Zod schema (e.g., BackendAffiliateStatsSchema) that defines required
fields (like swapCount, referralCount, etc.), parse the JSON with
BackendAffiliateStatsSchema.safeParse(await backendResponse.json()), and if
validation fails log the error and return the same 503/INVALID_RESPONSE
response; replace the type assertion with the validated object (backendData =
parsed.data) before using properties such as backendData.swapCount.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/swap-widget/src/hooks/useSwapRates.ts`:
- Around line 33-49: The affiliateAddress included in the queryKey of the
useSwapRates hook is misleading because it is not passed to apiClient.getRates
(so it doesn't affect the request); update the code to either (A) remove
affiliateAddress from UseSwapRatesParams and from the queryKey in useSwapRates
so the param is not accepted or used at all, or (B) keep it only for cache
separation but add an inline comment in useSwapRates and the type
UseSwapRatesParams clarifying "affiliateAddress is used only for cache key
separation and not sent to apiClient.getRates (apiClient config controls
headers)". Ensure references are to the queryKey array in useSwapRates and
apiClient.getRates so reviewers can verify the change.

---

Nitpick comments:
In `@packages/public-api/src/routes/affiliate.ts`:
- Around line 82-99: Remove the redundant clearTimeout(timeout) from the catch
block so the timer cleanup is only performed once in the finally block;
specifically, in the error handling inside the try/catch around the swap-service
request (the catch that checks for DOMException/AbortError and then logs and
responds), delete the clearTimeout(timeout) call and leave the single
clearTimeout(timeout) in the finally block to handle all paths.
- Around line 124-135: The code currently asserts the parsed JSON as
BackendAffiliateStats (backendData = (await backendResponse.json()) as
BackendAffiliateStats) without runtime validation; add a Zod schema (e.g.,
BackendAffiliateStatsSchema) that defines required fields (like swapCount,
referralCount, etc.), parse the JSON with
BackendAffiliateStatsSchema.safeParse(await backendResponse.json()), and if
validation fails log the error and return the same 503/INVALID_RESPONSE
response; replace the type assertion with the validated object (backendData =
parsed.data) before using properties such as backendData.swapCount.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56f1a952-8797-44ca-9779-7c6d1bcea48b

📥 Commits

Reviewing files that changed from the base of the PR and between d06414d and 5dfb283.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • packages/chain-adapters/package.json
  • packages/errors/package.json
  • packages/public-api/src/routes/affiliate.ts
  • packages/swap-widget/src/hooks/useSwapRates.ts
  • packages/swapper/src/types.ts
  • packages/types/package.json
  • packages/unchained-client/package.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/unchained-client/package.json
  • packages/swapper/src/types.ts
  • packages/types/package.json
  • packages/chain-adapters/package.json
  • packages/errors/package.json

- Validate localStorage affiliate address with isAddress() on read path
- Add AbortController timeout (10s) to swap-service fetch calls in status route
- Update misleading TOCTOU comment to clarify defense-in-depth intent
Copy link
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 (1)
packages/public-api/src/routes/status.ts (1)

126-131: Use the shared structured logger instead of raw console.error.

These paths are part of a new external-service integration, so losing fields like quoteId, txHash, upstream status, and timeout context will make production failures much harder to trace. Switching these to the API logger would also align with the repo rule for structured error metadata.

As per coding guidelines: "ALWAYS log errors for debugging using structured logging with relevant context and error metadata"

Also applies to: 149-150, 185-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/public-api/src/routes/status.ts` around lines 126 - 131, Replace raw
console.error calls used when registering swaps with the shared structured API
logger (e.g., apiLogger.error); specifically in the POST response check (use
postResponse and errorBody) and the catch block (use err) — and also the other
occurrences mentioned — so that each log call passes a descriptive message plus
a context object containing quoteId, txHash, upstreamStatus:
postResponse?.status, responseBody: errorBody (when present), error: err or
err.stack, and any timeout/context values available (e.g., timeoutMs or
requestId); update the log statements around the "register swap in swap-service"
logic (reference postResponse, errorBody, err) to use apiLogger.error with that
structured metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/public-api/src/routes/status.ts`:
- Around line 63-148: The code currently sets storedQuote.txHash,
storedQuote.registeredAt and status='submitted' before the POST to SWAP_SERVICE,
so a timed-out/failed POST leaves the swap unregistered; change the flow so
registration is idempotent: either (A) only set
storedQuote.txHash/registeredAt/status='submitted' after the POST to
`${SWAP_SERVICE_BASE_URL}/swaps` returns ok (use the same POST block around the
fetch call in this function), or (B) keep the local txHash but mark a flag
(e.g., storedQuote.swapRegistered=false) and on subsequent status checks (where
you call fetch(`${SWAP_SERVICE_BASE_URL}/swaps/${quoteId}`)) treat a 404 by
retrying the POST (reusing the existing POST body and AbortController logic) and
upon successful POST set storedQuote.swapRegistered=true and update quoteStore
via quoteStore.set(quoteId, storedQuote); update code paths around
storedQuote.txHash, quoteStore.set and the GET handling to implement one of
these approaches so registration is retried/idempotent across status calls.
- Around line 156-161: When swapServiceStatus is terminal ('SUCCESS' or
'FAILED') the computed local "status" variable is correct but storedQuote.status
isn't updated; update storedQuote.status to the terminal value and persist the
change back to quoteStore so it doesn't later revert (e.g., set
storedQuote.status = 'confirmed'|'failed' based on swapServiceStatus and then
call your quoteStore persistence method to save the updated storedQuote). Use
the existing identifiers swapServiceStatus, storedQuote, and status and invoke
the quoteStore method you already use elsewhere (e.g., update/save/put) to
persist the change.

---

Nitpick comments:
In `@packages/public-api/src/routes/status.ts`:
- Around line 126-131: Replace raw console.error calls used when registering
swaps with the shared structured API logger (e.g., apiLogger.error);
specifically in the POST response check (use postResponse and errorBody) and the
catch block (use err) — and also the other occurrences mentioned — so that each
log call passes a descriptive message plus a context object containing quoteId,
txHash, upstreamStatus: postResponse?.status, responseBody: errorBody (when
present), error: err or err.stack, and any timeout/context values available
(e.g., timeoutMs or requestId); update the log statements around the "register
swap in swap-service" logic (reference postResponse, errorBody, err) to use
apiLogger.error with that structured metadata.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72ba6b83-f362-453c-9f6f-037775eecfd4

📥 Commits

Reviewing files that changed from the base of the PR and between 5dfb283 and 8adc82b.

📒 Files selected for processing (2)
  • packages/public-api/src/routes/status.ts
  • src/hooks/useAffiliateTracking/useAffiliateTracking.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/useAffiliateTracking/useAffiliateTracking.ts

- Extract registerSwapInService helper for reuse across initial and retry paths
- Retry swap-service POST when GET returns 404 (recovers from failed initial registration)
- Persist confirmed/failed status back to quoteStore (prevents regression on later timeouts)
…tion

On Docker with copy import method, bare bin symlinks in node_modules/.bin/ may not resolve correctly. Using pnpm exec routes through pnpm's dependency graph instead of relying on PATH symlinks, fixing the 'openapi-generator-cli: not found' build failure on Railway.
@railway-app railway-app bot temporarily deployed to swap-widget / develop March 11, 2026 16:40 Inactive
@NeOMakinG NeOMakinG marked this pull request as draft March 11, 2026 16:45
@railway-app railway-app bot temporarily deployed to swap-widget / develop March 11, 2026 16:55 Inactive
@NeOMakinG NeOMakinG marked this pull request as ready for review March 11, 2026 17:42
@NeOMakinG NeOMakinG enabled auto-merge (squash) March 11, 2026 17:42
@NeOMakinG NeOMakinG merged commit 160813f into develop Mar 11, 2026
5 checks passed
@NeOMakinG NeOMakinG deleted the swap-tracking branch March 11, 2026 19:42
gomesalexandre added a commit that referenced this pull request Mar 12, 2026
* feat: add jito bundle support for all solana swappers

Add isOversized detection to Jupiter, Relay, and Across swappers.
When a Solana transaction exceeds the 1232-byte limit, the trade
execution layer automatically routes through Jito bundle splitting.

- Jupiter: trial-serialize compiled MessageV0 to check size
- Relay: same trial-serialize pattern for Solana quote items
- Across: check raw base64 tx bytes before deserialization
- Updated types JSDoc to reflect all-swappers scope

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

* feat: b2b affiliate tracking system (#12012)

* fix: bebop solana signing + reject amm-routed quotes (#12134)

* feat: chainflip lending - product docs (#12124)

* feat: chainflip lending - product docs

Regenerate product overview docs that were lost when gomes-bot got
soft-banned. Three docs covering all lending flows with live mainnet
data from RPC curls and xstate charts derived from the codebase:

- Deposit to State Chain (account creation + deposit channel flow)
- Borrow, Collateral & Repay (loan lifecycle + voluntary liquidation)
- Supply & Withdraw (supply positions + egress to on-chain wallet)

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

* fix: clarify batching sections as planned, not yet in UI

Address coderabbitai review - encodeBatch primitive exists in scale.ts
but UI state machines sign each operation individually. Reword both
batching sections to make it clear these are aspirational patterns.

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

* fix: scrub personal addresses from product docs

no doxxy baby

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

---------

Co-authored-by: gomes-bot <contact@0xgom.es>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jibles <premiumjibles@gmail.com>

* fix(affiliate-dashboard): add default API_URL to prevent 403 errors (#12146)

* feat: portless support for stable dev server URLs (#12130)

* fix: bebop solana ghost tx + malformed amm routes (#12148)

* fix: chainflip lending ui fixes and polish

* fix: chainflip lending ui fixes and polish

- add modal headers (title + close button) to all chainflip lending modals
- fix card context error (cardBody/cardFooter crash - missing card wrapper in modal)
- show asset symbol next to amount input in all modals (deposit, supply, collateral, borrow, repay, egress, withdraw)
- fiat/crypto toggle: properly convert value on mode switch, show $ prefix in fiat mode
- move "deposit to chainflip" tab to be first (leftmost)
- fix button widths to be equal 50/50 across all tabs (tooltip shouldWrapChildren span fix)
- fix "sign twice" inaccurate copy in deposit confirm - multi-step flow description
- allowance polling in deposit funding (guard against rpc propagation lag)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Use effectiveRefundAddress in DepositConfirm to always show refund
  destination in the confirm step
- Replace .toString() with .toFixed() in DepositInput and SupplyInput
  to prevent exponential notation for small fiat conversions

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: gomes-bot <contact@0xgom.es>

---------

Co-authored-by: gomes-bot <contact@0xgom.es>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Co-authored-by: Jibles <premiumjibles@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants