feat: affiliate fee split model (partnerBps + shapeshiftBps)#12391
Conversation
Aligns the dashboard with the updated API response shape where `bps` is now `partnerBps` to distinguish partner fees from the ShapeShift base cut. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename affiliateAddress to partnerAddress on AffiliateInfo and StoredQuote (swap-service registration maps to their partnerAddress field) - Add partnerBps/shapeshiftBps to auth middleware, stored quotes, and all response schemas (quote, rates, status) - Make partnerBps optional — only present for attributed partner swaps - Use quote.affiliateBps from swapper (supports free bridge swaps with 0 bps) - Remove affiliateAddress/partnerAddress from API responses (not actionable for clients; kept on StoredQuote for swap-service registration) - Remove allowMultiHop from request schemas (hardcode false — not supported through status tracking / swap-service) - Make sendAddress required on quote requests (needed for all chains; UTXO clients use account's 0/0 receive address) - Remove dead fields from StoredQuote (sellChainId, stepChainIds) - Simplify getQuote to single step (firstStep/lastStep collapsed) - Delegate approval check entirely to buildApprovalInfo (already handles non-EVM chains internally) - Add shared BpsFields for consistent OpenAPI descriptions across schemas - Add PartnerCodeResponse type to auth middleware - Fix swap-service registration: send partnerAddress (not affiliateAddress), include partnerBps and shapeshiftBps Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Make partnerBps optional on TradeQuote, TradeRate, and QuoteResponse - Remove affiliateAddress from QuoteResponse (no longer in API response) - Fix test fixtures: remove partnerBps from unattributed rate mocks, use `as unknown as QuoteResponse` for partial quote fixtures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avoids fractional scientific-notation output (e.g. "1.5e18") from BigNumber when converting to base-unit strings — downstream consumers expect integer-only strings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…countId - SwapServiceAffiliateSwap: require affiliateBps and partnerBps (no longer nullable / derived). Swap-service is now the source of truth and sends both fields explicitly. - getAffiliateSwaps: read partnerBps directly from the swap record instead of recomputing as max(0, affiliateBps - shapeshiftBps). - Swap registration payload: include buyAccountId (receiveAddress), now required by swap-service. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-end - useAffiliateTracking: drop the hardcoded SHAPESHIFT_CUT_BPS=10 and read partnerBps + shapeshiftBps directly from /v1/partner/:code. Store each as its own localStorage entry plus a derived affiliate_bps (sum) so consumers don't repeat the math. - Rename readStoredAffiliate -> readStoredPartnerAddress for consistency with the new partner-prefixed key names. Add readStoredShapeshiftBps and readStoredAffiliateBps; the latter defaults to DEFAULT_FEE_BPS so getAffiliateBps collapses to a one-line read. - tradeExecution: drop the inlined SHAPESHIFT_CUT_BPS math and orphaned bnOrZero/selectUsdRateByAssetId/sellAmountUsd plumbing. Send partnerAddress, partnerBps, affiliateBps, shapeshiftBps in the POST /swaps body, mirroring public-api's status/utils.ts shape. shapeshiftBps falls back to affiliateBps at the call site when no split is stored (no-partner case: SS keeps the full fee), keeping the "no stored split" semantics visible instead of buried behind a shared default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sponse Same-asset bridge swaps (e.g. ETH/Ethereum -> ETH/Arbitrum) settle with affiliateBps=0 on-chain even when the swap was attributed to a partner with a non-zero configured partnerBps. Two callers got this wrong: - calculatePartnerFeeAmountUsd: Path 1 was correctly skipped by the affiliateBps>0 guard, but Path 2 (inferred from volume * partnerBps / 10000) ran anyway and reported a phantom partner fee. Settlement is unaffected because swap-service's calculateFeeForSwap bails on verifiedBps=0, but the dashboard surfaced inflated revenue. Short-circuit on affiliateBps===0 so both paths agree that no fee earns the partner nothing. - AffiliateSwapSchema: response didn't include affiliateBps, leaving consumers unable to distinguish "fee-exempt swap" from "partner with low share" — both showed partnerBps=50 with no further context. Add the field so dashboard rows can detect affiliateBps===0 && partnerBps>0 and render accordingly. Mirror schema in affiliate-dashboard's useAffiliateSwaps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
public-api: - calculatePartnerFeeAmountUsd: cap partner share at 100% of capture via Math.min(partnerBps/affiliateBps, 1) implemented as an explicit branch so BigNumber precision is preserved; collapse the early-guard tree into a single `if (!partnerBps || !affiliateBps) return null`. - getQuote/getRates: restore `lastStep` for buyAmountAfter/Before; keep `step` for first-hop-only fields. No-op for current single-step swappers; correct for any future multi-step quote (e.g. THORChain longtail). - status: re-add partnerAddress to SwapStatusResponse alongside the bps fields. - auth middleware: validate the /v1/partner response through a zod schema so malformed upstream data falls back to the default-bps path instead of propagating NaN through req.affiliateInfo. - BpsFields: docstring clarifies affiliateBps is the swapper-reported total, not strictly partnerBps + shapeshiftBps (fee-exempt swaps break the sum). web: - useAffiliateTracking: one-shot module-load migration clears pre-v2 partner storage (previously stored partner+10 under PARTNER_BPS_KEY) so returning users re-resolve cleanly instead of reading legacy totals as partner-only values. Collapse five duplicated readStored* helpers into one shared reader. Drop denormalized AFFILIATE_BPS_KEY in favour of lazy sum at read time. - tradeExecution: Number(affiliateBps) so all three bps fields in POST /swaps share the type swap-service's CreateSwapDto declares. affiliate-dashboard: - AffiliateConfigSchema: add shapeshiftBps to match the public-api response shape so future UI can display the platform-fee breakdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR refactors the affiliate fee structure across the entire ShapeShift stack, splitting the single ChangesAffiliate Fee Structure Refactoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes This PR involves substantial changes across the entire stack with multiple interconnected layers. The refactoring affects data models, API contracts, middleware logic, multiple route handlers, client-side storage and resolution, and test fixtures. While most changes are consistent patterns (field renames and structure changes), the complexity arises from the breadth of affected files, the logic changes in fee calculation and partner resolution, and the need to verify data flow across all layers.
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@packages/public-api/src/routes/status/utils.ts`:
- Around line 25-27: The BPS fields currently convert values with Number(...)
which turns invalid/missing inputs into NaN (serialized as null) and the
partnerBps ternary drops numeric 0; update the mapping for partnerBps,
affiliateBps, and shapeshiftBps to validate and preserve zero by only converting
when storedQuote.<field> is not null/undefined and is a valid finite number
string/number (e.g., use Number.isFinite(Number(val)) or a small helper like
toNumberOrUndefined(val) that returns undefined for null/undefined/invalid and
returns 0 for "0"), and otherwise set the property to undefined so JSON won’t
contain null/NaN values. Ensure you change the three references partnerBps,
affiliateBps, and shapeshiftBps in utils.ts accordingly.
In `@src/hooks/useAffiliateTracking/useAffiliateTracking.ts`:
- Around line 72-85: resolvePartnerCode currently parses remote JSON and returns
it without checking shape; add a runtime guard that verifies the parsed object
matches the PartnerData contract (presence and types of required fields) before
returning or letting it be cached in localStorage. Modify resolvePartnerCode to
validate the decoded JSON (e.g., check required keys and primitive types on the
object returned by await response.json()), return null if validation fails, and
ensure any place that stores partner info to localStorage only writes when the
value is a valid PartnerData. Use the PartnerData type and the
resolvePartnerCode function name to locate where to add the guard.
- Around line 138-143: readStoredAffiliateBps can return "NaN" if stored strings
are non-numeric; fix by parsing and validating both stored values (use
readStored(PARTNER_BPS_KEY) and readStored(SHAPESHIFT_BPS_KEY)), convert each to
Number, and check with Number.isFinite (or !Number.isNaN) — if either parsed
value is invalid, return DEFAULT_FEE_BPS; otherwise return String(parsedPartner
+ parsedShapeshift). Ensure you reference readStoredAffiliateBps,
PARTNER_BPS_KEY, SHAPESHIFT_BPS_KEY, DEFAULT_FEE_BPS, and readStored when making
the change.
In `@src/lib/tradeExecution.ts`:
- Around line 223-240: Validate numeric fee fields before sending the POST to
/swaps: when building the payload in the axios.post call, parse and guard
partnerBps (from readStoredPartnerBps()), shapeshiftBps (from
readStoredShapeshiftBps()) and any use of affiliateBps so they are finite
numbers (e.g., Number(...) and Number.isFinite check); if a parsed value is NaN
or not finite, omit the field (use undefined) or fallback to a safe default (for
shapeshiftBps fallback to affiliateBps only if affiliateBps is valid). Update
the payload construction around partnerBps and shapeshiftBps in
tradeExecution.ts so invalid localStorage values do not result in NaN being
posted.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bb47ab28-1374-42ad-a02e-5d66b26af6bc
📒 Files selected for processing (29)
packages/affiliate-dashboard/src/components/ConfigBar.tsxpackages/affiliate-dashboard/src/components/settings/ConfigSummaryCard.tsxpackages/affiliate-dashboard/src/components/settings/RegisterCard.tsxpackages/affiliate-dashboard/src/components/settings/SettingsTab.tsxpackages/affiliate-dashboard/src/hooks/useAffiliateConfig.tspackages/affiliate-dashboard/src/hooks/useAffiliateSwaps.tspackages/public-api/src/lib/quoteStore.test.tspackages/public-api/src/lib/quoteStore.tspackages/public-api/src/middleware/auth.tspackages/public-api/src/routes/affiliate/calculatePartnerFeeAmountUsd.test.tspackages/public-api/src/routes/affiliate/calculatePartnerFeeAmountUsd.tspackages/public-api/src/routes/affiliate/getAffiliateSwaps.tspackages/public-api/src/routes/affiliate/types.tspackages/public-api/src/routes/quote/getQuote.tspackages/public-api/src/routes/quote/types.tspackages/public-api/src/routes/rates/getRates.tspackages/public-api/src/routes/rates/types.tspackages/public-api/src/routes/status/getSwapStatus.tspackages/public-api/src/routes/status/types.tspackages/public-api/src/routes/status/utils.tspackages/public-api/src/types.tspackages/swap-widget/src/machines/__tests__/swapMachine.test.tspackages/swap-widget/src/machines/__tests__/types.test.tspackages/swap-widget/src/types/index.tspackages/swapper/src/thorchain-utils/getL1RateOrQuote.tssrc/hooks/useAffiliateTracking/index.tssrc/hooks/useAffiliateTracking/useAffiliateTracking.tssrc/lib/fees/utils.tssrc/lib/tradeExecution.ts
Description
Introduces the split-fee affiliate model across the stack.
affiliateBpsis now the total on-chain fee whilepartnerBpsandshapeshiftBpsare the configured intent snapshotted at quote time. Swap-service owns the source of truth; web, public-api, swap-widget, and the affiliate dashboard are updated to consume and forward the three values consistently.Pairs with: companion microservices PR on
feat/swap-bps-and-partner-fields(separate repo).Issue (if applicable)
closes #
Risk
Medium. Touches the affiliate fee plumbing end-to-end and the public-api request/response surface. No on-chain transaction changes. Verified manually against swap-service local; existing test suites pass after fixture updates.
All swappers indirectly through affiliate-fee plumbing — no protocol-specific changes. THORChain
getL1RateOrQuoteswitches.toString()→.toFixed(0)on integer base-unit strings (correctness, not behavioral).Breaking API changes — call out for integrators
POST /v1/swap/quote:sendAddressis now required (was optional).POST /v1/swap/quoteand/v1/swap/rates:allowMultiHoprequest param removed; always single-hop.GET /v1/swap/statusresponse:affiliateAddressremoved;partnerAddressadded.affiliateconfig endpoints now returnpartnerBps+shapeshiftBps(wasbps).Testing
Engineering
pnpm run lint --fix && pnpm run type-check(both packages)pnpm testinpackages/public-api(includes newcalculatePartnerFeeAmountUsdregression tests for theMath.min(partnerBps/affiliateBps, 1)cap and the fee-exempt branch)partner_bps,shapeshift_bps,partner_codewritten to localStorage with the partner's share (not the legacy total + 10). Then trade and inspect thePOST /swapspayload — should send all three bps as numbers pluspartnerAddress/partnerCode.useAffiliateTrackingclears pre-v2 entries once and sets a version marker; returning users see "no partner" until they re-hit a partner URL.Operations
No infra changes. Deploy after the companion swap-service PR lands so the
/v1/partner/:codeand affiliate config responses includeshapeshiftBps.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Updates
Refactor