Rework referral tracking flow#556
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 49 minutes and 39 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (17)
📝 WalkthroughWalkthroughPR integrates wallet signature verification across API key and referral systems. API key creation now uses deterministic signed messages instead of nonce-based validation. A new referral feature lets users generate and share codes; codes are captured from URL parameters and trigger attribution on transaction confirmation via fire-and-forget POST. ChangesWallet Signature Infrastructure, Referrals, and Post-Transaction Analytics
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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.
Code Review
This pull request introduces a referral and platform fee tracking system, including new API routes, a tracking provider, and rewards UI components, while refactoring API key generation to use a shared wallet signature verification utility. Feedback focuses on addressing potential signature verification failures for L2-only smart contract wallets due to a hardcoded Mainnet chain ID, caching generated referral codes in local storage to prevent redundant signature prompts, simplifying URL query parameter parsing, and refining boolean logic in fee calculations to avoid unexpected type coercion.
| @@ -0,0 +1,23 @@ | |||
| import { SupportedNetworks } from '@/utils/supported-networks'; | |||
|
|
|||
| export const WALLET_SIGNATURE_CHAIN_ID = SupportedNetworks.Mainnet; | |||
There was a problem hiding this comment.
Hardcoding WALLET_SIGNATURE_CHAIN_ID to SupportedNetworks.Mainnet will break signature verification for smart contract wallets (such as Safe) that are only deployed on L2 networks (e.g., Base, Arbitrum, Optimism).
Since verifyMessage from viem/actions uses ERC-1271 to verify contract signatures, it must query the chain where the contract is deployed. If a contract wallet only exists on an L2, querying Mainnet will fail. Consider allowing the client to specify the chain ID of their connected wallet, or verifying against the chain where the wallet is active, while still using a server-reconstructed message.
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
To improve user experience and avoid redundant wallet signatures, we can cache the generated referral code in localStorage (using the project's local-storage-fallback adapter) keyed by the wallet address. This avoids forcing the user to sign a wallet message every single time they visit the rewards page.
| } | |
| } | |
| export function getOwnReferralCode(address: string): string | null { | |
| if (!canUseReferralStorage) return null; | |
| try { | |
| return referralStorage.getItem(`monarch_own_referral_code_${address.toLowerCase()}`); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export function storeOwnReferralCode(address: string, code: string): void { | |
| if (!canUseReferralStorage) return; | |
| try { | |
| referralStorage.setItem(`monarch_own_referral_code_${address.toLowerCase()}`, code); | |
| } catch {} | |
| } |
| import { Modal, ModalBody, ModalFooter, ModalHeader } from '@/components/common/Modal'; | ||
| import { Button } from '@/components/ui/button'; | ||
| import { MONARCH_PRIMARY } from '@/constants/chartColors'; | ||
| import { getWalletSignatureMessage } from '@/utils/walletSignature'; |
There was a problem hiding this comment.
Import the new getOwnReferralCode and storeOwnReferralCode helpers from @/utils/referrals to support caching the user's own referral code.
| import { getWalletSignatureMessage } from '@/utils/walletSignature'; | |
| import { getWalletSignatureMessage } from '@/utils/walletSignature'; | |
| import { getOwnReferralCode, storeOwnReferralCode } from '@/utils/referrals'; |
| useEffect(() => { | ||
| setCode(null); | ||
| setError(null); | ||
| setRequestState('idle'); | ||
| setCopied(false); | ||
| setIsModalOpen(false); | ||
| }, [address, account]); |
There was a problem hiding this comment.
Use the new getOwnReferralCode helper to load the cached referral code on mount or address change, preventing unnecessary wallet signature requests.
useEffect(() => {
setError(null);
setRequestState('idle');
setCopied(false);
setIsModalOpen(false);
if (address) {
setCode(getOwnReferralCode(address));
} else {
setCode(null);
}
}, [address, account]);
| setCode(body.code); | ||
| return `${window.location.origin}/?ref=${body.code}`; |
There was a problem hiding this comment.
Store the newly created referral code in the cache once it is successfully fetched from the API.
| setCode(body.code); | |
| return `${window.location.origin}/?ref=${body.code}`; | |
| setCode(body.code); | |
| if (address) { | |
| storeOwnReferralCode(address, body.code); | |
| } | |
| return `${window.location.origin}/?ref=${body.code}`; |
| useEffect(() => { | ||
| const url = new URL(window.location.href); | ||
| const code = url.searchParams.get('ref') ?? url.searchParams.get('referral'); | ||
| if (code) storeReferralCodeOnce(code); | ||
| }, []); |
There was a problem hiding this comment.
Instead of parsing the entire window.location.href with new URL(), we can use new URLSearchParams(window.location.search) directly, which is simpler and more efficient.
| useEffect(() => { | |
| const url = new URL(window.location.href); | |
| const code = url.searchParams.get('ref') ?? url.searchParams.get('referral'); | |
| if (code) storeReferralCodeOnce(code); | |
| }, []); | |
| useEffect(() => { | |
| const params = new URLSearchParams(window.location.search); | |
| const code = params.get('ref') ?? params.get('referral'); | |
| if (code) storeReferralCodeOnce(code); | |
| }, []); |
| const platformFeeEvents = useMemo( | ||
| () => | ||
| feeAmount && feeAmount > 0n | ||
| ? [ | ||
| { | ||
| source: 'smart-rebalance', | ||
| tokenAddress: groupedPosition.loanAssetAddress as Address, | ||
| amountRaw: feeAmount, | ||
| }, | ||
| ] | ||
| : [], | ||
| [feeAmount, groupedPosition.loanAssetAddress], | ||
| ); |
There was a problem hiding this comment.
In TypeScript/JavaScript, if feeAmount is 0n, feeAmount && feeAmount > 0n evaluates to 0n. While falsy, it's safer and cleaner to use feeAmount != null && feeAmount > 0n to avoid unexpected type coercion or lint issues (especially with Biome's strict boolean rules).
| const platformFeeEvents = useMemo( | |
| () => | |
| feeAmount && feeAmount > 0n | |
| ? [ | |
| { | |
| source: 'smart-rebalance', | |
| tokenAddress: groupedPosition.loanAssetAddress as Address, | |
| amountRaw: feeAmount, | |
| }, | |
| ] | |
| : [], | |
| [feeAmount, groupedPosition.loanAssetAddress], | |
| ); | |
| const platformFeeEvents = useMemo( | |
| () => | |
| feeAmount != null && feeAmount > 0n | |
| ? [ | |
| { | |
| source: 'smart-rebalance', | |
| tokenAddress: groupedPosition.loanAssetAddress as Address, | |
| amountRaw: feeAmount, | |
| }, | |
| ] | |
| : [], | |
| [feeAmount, groupedPosition.loanAssetAddress], | |
| ); |
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 `@app/api/api-keys/route.ts`:
- Around line 65-85: The fetch to adminUrl (where response is assigned) lacks an
abort/timeout, so slow gateway calls can hang; wrap the request using an
AbortController (create controller, pass controller.signal to fetch) and set a
timer (e.g., const timeoutId = setTimeout(() => controller.abort(), <timeout
ms>)) before calling fetch, then clearTimeout(timeoutId) once the fetch
resolves; also catch the abort error from fetch (check for DOMException name
'AbortError' or similar) to return a proper timeout response instead of letting
the route hang.
In `@src/components/providers/ReferralTrackingProvider.tsx`:
- Around line 7-11: The current useEffect in ReferralTrackingProvider only runs
once on mount so client-side navigations that add ?ref=... are missed; update
the effect to depend on the current route/search params so it re-runs on client
route changes. Specifically, import and use Next.js App Router hooks (e.g.,
usePathname and/or useSearchParams) or another router-derived value inside the
effect dependency array, read the 'ref' or 'referral' param there, and call
storeReferralCodeOnce(code) when present; keep the logic in the
ReferralTrackingProvider and ensure any window access remains inside the
client-only effect.
In `@src/hooks/useTransactionWithToast.tsx`:
- Around line 46-50: When sending a transaction, capture and persist the
submitting wallet's address at submission time (e.g., set submittingWalletRef
when calling sendTransaction / sendTransactionAsync) and use that stored value
for later side-effects (analytics, referral attribution, and trackPlatformFees)
instead of reading the current connection's address from useConnection() at
confirmation time; update the code paths that read address in the
confirmation/error handlers (those that reference reportedErrorKeyRef,
handledConfirmationHashRef, and call trackPlatformFees) to use the
submittingWalletRef so attribution always reflects the wallet that originally
submitted the tx.
In `@src/utils/dataApiInternal.ts`:
- Around line 13-21: The internal POST fetch currently has no timeout and can
hang; wrap the fetch in an AbortController, pass controller.signal into the
fetch options (alongside method/headers/body/cache), and set a timer (e.g.,
setTimeout) to call controller.abort() after a short configurable timeout,
clearing the timer on successful resolution; update the code around the fetch
call that uses INTERNAL_ADMIN_HEADER and adminKey so callers fail fast when the
data API stalls.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f21a891f-2101-4c2e-ba56-caef6eddd14d
📒 Files selected for processing (22)
.env.local.exampleapp/api/api-keys/route.tsapp/api/platform-fees/route.tsapp/api/referrals/attribute/route.tsapp/api/referrals/code/route.tsapp/layout.tsxdocs/TECHNICAL_OVERVIEW.mddocs/VALIDATIONS.mdsrc/components/providers/ReferralTrackingProvider.tsxsrc/features/api-keys/api-key-console-view.tsxsrc/features/rewards/referral-rewards-block.tsxsrc/features/rewards/rewards-view.tsxsrc/hooks/useLeverageTransaction.tssrc/hooks/usePlatformFeeTracking.tssrc/hooks/useRebalanceExecution.tssrc/hooks/useSmartRebalance.tssrc/hooks/useTransactionWithToast.tsxsrc/utils/apiKeyRequest.tssrc/utils/dataApiInternal.tssrc/utils/referrals.tssrc/utils/serverWalletSignature.tssrc/utils/walletSignature.ts
💤 Files with no reviewable changes (1)
- src/utils/apiKeyRequest.ts
d285e9c to
a24b8db
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/api/api-keys/route.ts (1)
105-109:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winValidate
keybefore returning it.
apiKeyis checked, butkeyis forwarded asunknown. If the admin service returns a non-string there, this route still emits a malformed success payload to the client.Suggested fix
- if (typeof body.apiKey !== 'string') { - return NextResponse.json({ error: 'Gateway did not return an API key.' }, { status: 502 }); - } - - return NextResponse.json({ apiKey: body.apiKey, key: body.key }, { status: 201 }); + if (typeof body.apiKey !== 'string') { + return NextResponse.json({ error: 'Gateway did not return an API key.' }, { status: 502 }); + } + + if (body.key !== undefined && typeof body.key !== 'string') { + return NextResponse.json({ error: 'Gateway returned a malformed API key payload.' }, { status: 502 }); + } + + return NextResponse.json( + body.key === undefined ? { apiKey: body.apiKey } : { apiKey: body.apiKey, key: body.key }, + { status: 201 }, + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/api/api-keys/route.ts` around lines 105 - 109, The handler validates body.apiKey but not body.key, so ensure you check that body.key is a string before returning it: in the same function that inspects body.apiKey (the block using body.apiKey and returning NextResponse.json({ apiKey: body.apiKey, key: body.key }, ...)), add a type check for typeof body.key === 'string' and return a 502 NextResponse.json({ error: 'Gateway did not return a valid key.' }, { status: 502 }) if the check fails; only return the success payload with key when both validations pass.
🤖 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 `@src/hooks/useTransactionWithToast.tsx`:
- Around line 122-135: The referral code retrieved by getStoredReferralCode() is
never removed, causing repeated attributions; add a clearStoredReferralCode()
helper in src/utils/referrals.ts and call it after a successful POST in the
block inside useTransactionWithToast where you POST to
'/api/referrals/attribute' (i.e., after the fetch resolves without error) so the
stored code is cleared and not reused for subsequent confirmed transactions;
ensure you only clear it on successful submission and keep the existing .catch
behaviour for failures.
---
Outside diff comments:
In `@app/api/api-keys/route.ts`:
- Around line 105-109: The handler validates body.apiKey but not body.key, so
ensure you check that body.key is a string before returning it: in the same
function that inspects body.apiKey (the block using body.apiKey and returning
NextResponse.json({ apiKey: body.apiKey, key: body.key }, ...)), add a type
check for typeof body.key === 'string' and return a 502 NextResponse.json({
error: 'Gateway did not return a valid key.' }, { status: 502 }) if the check
fails; only return the success payload with key when both validations pass.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e7e6468f-bca0-4d4b-81d6-331dbe78e84a
📒 Files selected for processing (17)
.env.local.exampleapp/api/api-keys/route.tsapp/api/referrals/attribute/route.tsapp/api/referrals/code/route.tsapp/layout.tsxdocs/TECHNICAL_OVERVIEW.mddocs/VALIDATIONS.mdsrc/components/providers/ReferralTrackingProvider.tsxsrc/features/api-keys/api-key-console-view.tsxsrc/features/rewards/referral-rewards-block.tsxsrc/features/rewards/rewards-view.tsxsrc/hooks/useTransactionWithToast.tsxsrc/utils/apiKeyRequest.tssrc/utils/dataApiInternal.tssrc/utils/referrals.tssrc/utils/serverWalletSignature.tssrc/utils/walletSignature.ts
💤 Files with no reviewable changes (1)
- src/utils/apiKeyRequest.ts
✅ Files skipped from review due to trivial changes (1)
- docs/VALIDATIONS.md
🚧 Files skipped from review as they are similar to previous changes (8)
- src/components/providers/ReferralTrackingProvider.tsx
- src/utils/dataApiInternal.ts
- src/features/rewards/rewards-view.tsx
- src/utils/serverWalletSignature.ts
- src/utils/walletSignature.ts
- app/api/referrals/attribute/route.ts
- src/features/api-keys/api-key-console-view.tsx
- src/features/rewards/referral-rewards-block.tsx
This removes the speculative platform-fee logging path and keeps the PR focused on API-key creation plus referral-code ownership and first-touch attribution. Referral code creation reuses a Mainnet wallet-ownership proof, the Rewards block only exposes a link for the connected wallet after signing, and post-confirmation attribution forwards the receipt sender/referral code without blocking transaction success. Constraint: Do not track fee amounts or parse transaction logs in this PR. Rejected: Platform fee event hooks and ERC-20 receipt log validation | user explicitly reduced scope to referral attribution only. Rejected: Cloudflare/browser gateway fallback origins | private service origins must stay only in deployment secrets. Confidence: high Scope-risk: moderate Directive: Do not reintroduce platform-fee tracking here without a separate design for fee accounting. Tested: pnpm exec biome check --write <changed files>; pnpm typecheck; pnpm build; git diff --check Not-tested: End-to-end Vercel to data-api write with production secrets; npx ultracite fix/check blocked by existing biome.jsonc unknown-rule keys.
a24b8db to
04cc65b
Compare
Summary
Verification
Summary by CodeRabbit
Release Notes
New Features
Documentation