Skip to content

refactor(swap-verification): slim verifier contract and unify logs - #31

Merged
kaladinlight merged 1 commit into
developfrom
refactor/swap-verification-contract
Apr 27, 2026
Merged

refactor(swap-verification): slim verifier contract and unify logs#31
kaladinlight merged 1 commit into
developfrom
refactor/swap-verification-contract

Conversation

@kaladinlight

@kaladinlight kaladinlight commented Apr 27, 2026

Copy link
Copy Markdown
Member

Description

Reduces the verifier contract to what callers actually consume, drops dead return paths, and unifies per-verifier logs behind a small helper.

SwapVerificationResult shape change. swapId, swapperName, details were echoed by every verifier but only ever inspected by the now-deleted POST /:id/verify-affiliate HTTP route. With that route gone, those fields are pure noise for the only remaining caller (reconcileSwap), which already has the swap object in scope. Removing them shrinks per-verifier boilerplate considerably.

New required fields. actualBuyAmountCryptoBaseUnit and actualAffiliateFeeAmountCryptoBaseUnit are added as string | undefined (required keys, optional values) so the type system forces every verifier — current and future — to explicitly decide whether it can produce them. reconcileSwap writes both to the swap row alongside isAffiliateVerified. No verifier extracts them yet; per-swapper extraction lands in follow-ups (NearIntents first).

pollSwapStatus no longer round-trips verification metadata. isAffiliateVerified / affiliateVerificationDetails were declared on SwapStatusResponse but never read by the polling service (the only consumer). External consumers read those columns straight off the swap row via GET /swaps/:id. reconcileSwap becomes Promise<void> — its job is the side-effect write. SwapReconciliation type is removed. UpdateSwapStatusDto drops actualBuyAmountCryptoBaseUnit since the verifier owns that column now.

Logs. Pulled the pipe-separated pattern into a logVerification helper in verification/utils.ts. Stable column shape across all 13 verifiers that emit logs:

<Name> verification | swapId=<id> | affiliate=<addr> (<bps> bps) | sell=<n> | buy=<n> | fee=<n> | <extras>

Missing amounts render as =none so operators can grep "fee=none" to find unverified swaps. Verifier-specific context (Thorchain memo, Across status, Portals feeAmount) lives in the extras arg.

Type tightening.

  • CreateSwapDto.swapperName: stringSwapperName — catches typos in internal callers at compile time.
  • affiliateFeeAsset.ts strategy map is now keyed by SwapperName enum, exhaustive over every variant (picks up ArbitrumBridge / Debridge / Test which previously fell through to null silently).

Cleanup. Dead variables that only fed the removed details blocks removed: feeAmountUsd (Portals), referrerFeeUnits (Stonfi), fillTxnRef (Across).

Testing

  • swap-service builds and boots
  • in-process polling lifecycle still updates swap status and reconciles affiliate verification
  • reconcile writes the two new actual-amount columns (currently always NULL in DB since no verifier extracts yet — follow-up PRs will fill these in per-swapper)
  • log lines on each verifier now follow the unified pipe format
  • no external consumer reads isAffiliateVerified / affiliateVerificationDetails from SwapStatusResponse (verified via grep across this repo and shapeshift/web — only consumers read them directly off the swap row)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved swap verification and reconciliation handling to ensure accurate tracking of buy amounts and affiliate fees.
  • Refactor

    • Simplified swap status API responses by removing internal affiliate verification details.
    • Enhanced type safety for swapper configuration.
    • Centralized verification logging for better diagnostics.

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@kaladinlight has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 43 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d22c510-0d98-42dd-8b88-d20d4c896c95

📥 Commits

Reviewing files that changed from the base of the PR and between 9d322f8 and f8919bb.

📒 Files selected for processing (6)
  • apps/swap-service/src/swaps/swaps.service.ts
  • apps/swap-service/src/swaps/types.ts
  • apps/swap-service/src/utils/affiliateFeeAsset.ts
  • apps/swap-service/src/verification/swap-verification.service.ts
  • apps/swap-service/src/verification/utils.ts
  • packages/shared-types/src/index.ts
📝 Walkthrough

Walkthrough

The pull request refactors swap reconciliation and verification flows, treating reconciliation as side-effect-only, removing affiliate verification fields from API responses, and introducing centralized verification logging. Type safety is improved through stronger enum-based typing of swapper names, and actual amounts are now explicitly tracked in verification results.

Changes

Cohort / File(s) Summary
Type Definitions
packages/shared-types/src/index.ts
Removes affiliate verification fields from SwapStatusResponse, updates SwapVerificationResult to track explicit actual amounts instead of generic details map, removes actualBuyAmountCryptoBaseUnit from UpdateSwapStatusDto, and strengthens swapperName typing in CreateSwapDto.
Reconciliation & Type Removal
apps/swap-service/src/swaps/swaps.service.ts, apps/swap-service/src/swaps/types.ts
Converts reconcileSwap to side-effect-only operation returning Promise<void>, removes SwapReconciliation type, and ceases surfacing affiliate verification details in response objects while persisting them to database.
Verification Service Refactoring
apps/swap-service/src/verification/swap-verification.service.ts, apps/swap-service/src/verification/utils.ts
Introduces centralized logVerification helper function, refactors verifier methods to use structured logging, updates SwapVerificationResult construction to include explicit actual amounts, and simplifies verifyNearIntents logic.
Utility Type Safety
apps/swap-service/src/utils/affiliateFeeAsset.ts
Strengthens function signature with SwapperName enum typing, extends fee asset strategy union with 'none' option, and remaps strategy assignments to use typed enum keys.

Sequence Diagram

sequenceDiagram
    participant PollService as Poll Flow
    participant SwapService as Swaps Service
    participant VerifyService as Verification Service
    participant Database as Database/Prisma
    participant Logger as Logger

    PollService->>SwapService: pollSwapStatus(swapId)
    SwapService->>VerifyService: verifySwap(swapDetails)
    VerifyService->>VerifyService: Compute verification result<br/>(actualBuyAmount, actualFeeAmount)
    VerifyService->>Logger: logVerification(result)
    Logger->>Logger: Format log segments<br/>(affiliate status, amounts)
    VerifyService-->>SwapService: SwapVerificationResult<br/>(without swapId/swapperName)
    SwapService->>SwapService: reconcileSwap(result)<br/>(side-effect only)
    SwapService->>Database: prisma.swap.update<br/>(persist verification amounts)
    Database-->>SwapService: Updated
    SwapService->>Database: fetch swap data<br/>(no affiliate fields)
    Database-->>SwapService: SwapStatusResponse<br/>(affiliate details not included)
    SwapService-->>PollService: SwapStatusResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Whiskers twitching, we've refined our ways,
Reconciliation now works behind the scenes, not in displays,
Verification logs dance through structured light,
Type safety blooms—SwapperName keeps things tight,
Actual amounts tracked true and clear, hopping toward the right! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and accurately describes the main changes: refactoring the swap verification contract to be slimmer and unifying the logging mechanism across verifiers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/swap-verification-contract

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.

The SwapVerificationResult shape carried fields that no caller actually
read — swapId, swapperName, and details were echoed by every verifier but
only ever inspected by the now-deleted POST /:id/verify-affiliate route.
Reducing the contract to what reconcileSwap consumes makes the verifier's
"output spec" reflect reality and shrinks the per-verifier boilerplate.

Threading through that change:
- SwapVerificationResult drops swapId / swapperName / details and adds
  required actualBuyAmountCryptoBaseUnit / actualAffiliateFeeAmountCryptoBaseUnit
  as string | undefined (required keys force every verifier to consciously
  decide whether it can produce them — silent omission is no longer possible).
- pollSwapStatus no longer round-trips isAffiliateVerified /
  affiliateVerificationDetails — the only caller never read them, and external
  consumers read those columns directly off the swap row. SwapStatusResponse
  shrinks accordingly; SwapReconciliation type is removed; reconcileSwap is
  now Promise<void>, doing only the side-effect write (and now persists the
  two new actual-amount columns from whatever the verifier returns).
- updateSwapStatus drops actualBuyAmountCryptoBaseUnit — there's no producer
  anywhere after the HTTP route was deleted, and it's now sourced from
  verification.

Verifier mechanics:
- All 16 verifiers' returns slimmed; details blocks gone; dead variables
  that only fed details cleaned up (feeAmountUsd, referrerFeeUnits,
  fillTxnRef).
- Per-verifier summary logs unified via a logVerification helper in
  verification/utils.ts. Pipe-format with stable column shape:
    <Name> verification | swapId=<id> | affiliate=<addr> (<bps> bps) | sell=<n> | buy=<n> | fee=<n> | <extras>
  Missing amounts render as `=none` so operators can grep for unverified
  swaps; verifier-specific context (memo, status, feeAmount) lives in extras.

Type tightening:
- CreateSwapDto.swapperName: string → SwapperName.
- affiliateFeeAsset's strategy map keyed by SwapperName (exhaustive — picks
  up ArbitrumBridge / Debridge / Test which previously fell through to null).

No verifier extracts actualBuy/actualFee yet; those land per-swapper in
follow-ups (NearIntents first).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
apps/swap-service/src/verification/swap-verification.service.ts (1)

220-228: ⚠️ Potential issue | 🟡 Minor

Inconsistent logging: Relay/Chainflip/0x verifiers don't call logVerification on the success path.

The PR description states "All verifier logs follow a pipe-separated stable format" via logVerification, but verifyRelay (Lines 220-228), verifyChainflip (Lines 639-647), and verifyZrx (Lines 720-728) construct and return the result directly without calling logVerification. This leaves a gap in the unified format you're trying to establish — three high-volume swappers will silently skip the new log line.

🛠 Suggested fix (apply analogously to all three verifiers)
-      return {
+      const result: SwapVerificationResult = {
         isVerified: true,
         hasAffiliate: hasShapeshiftAffiliate,
         affiliateBps,
         affiliateAddress,
         verifiedSellAmountCryptoBaseUnit,
         actualBuyAmountCryptoBaseUnit: undefined,
         actualAffiliateFeeAmountCryptoBaseUnit: undefined,
       }
+
+      logVerification(this.logger, SwapperName.Relay, swapId, result)
+
+      return result

Also applies to: 639-647, 720-728

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

In `@apps/swap-service/src/verification/swap-verification.service.ts` around lines
220 - 228, verifyRelay, verifyChainflip and verifyZrx currently return success
results directly and skip the unified log line; update each verifier to call
logVerification(...) with the final result object (same fields you return:
isVerified, hasAffiliate, affiliateBps, affiliateAddress,
verifiedSellAmountCryptoBaseUnit, actualBuyAmountCryptoBaseUnit,
actualAffiliateFeeAmountCryptoBaseUnit) before returning it so the
pipe-separated stable format is emitted for these high-volume verifiers; locate
calls in functions verifyRelay, verifyChainflip and verifyZrx and invoke
logVerification(contextOrParams, result) (matching the existing logVerification
signature used elsewhere) and then return the logged result.
🧹 Nitpick comments (3)
apps/swap-service/src/verification/utils.ts (1)

16-42: Helper looks clean; one optional consideration on duplicate keys.

If a caller passes an extra key that collides with a base segment (e.g., swapId, sell, buy, fee, affiliate), the line will end up with two segments using the same key. Consider either dropping conflicting keys or documenting reserved key names. This is purely defensive — current call sites pass status, memo, feeAmount, none of which collide.

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

In `@apps/swap-service/src/verification/utils.ts` around lines 16 - 42, The
logVerification helper can produce duplicate keys if callers include extras that
collide with base segments; update logVerification to defensively drop or rename
conflicting keys in the incoming extra object before building segments: define a
reserved set (e.g., 'swapId','sell','buy','fee','affiliate' and any other base
keys used in segments), then filter Object.entries(extra) to exclude any key in
that reserved set (or alternatively prefix remaining keys), and finally build
the extras array from the filtered entries so no duplicate keys appear in the
joined log; keep the function name logVerification and the same output format.
apps/swap-service/src/utils/affiliateFeeAsset.ts (1)

27-41: Tighten exhaustiveness now that the strategy map is Record<SwapperName, FeeAssetStrategy>.

Two small follow-ups:

  1. if (!strategy) return null (Line 29) is now unreachable since the map is exhaustive over the SwapperName enum.
  2. The 'none' case is handled implicitly via default, which masks future additions to FeeAssetStrategy. Making 'none' explicit and using a never-typed default lets TypeScript catch newly-added strategies.
♻️ Proposed refactor
 export function resolveAffiliateFeeAssetId(swapperName: SwapperName, sellAsset: Asset, buyAsset: Asset): string | null {
   const strategy = SWAPPER_FEE_STRATEGY[swapperName]
-  if (!strategy) return null

   switch (strategy) {
     case 'buy_asset':
       return buyAsset.assetId
     case 'sell_asset':
       return sellAsset.assetId
     case 'fixed_base':
       return 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'
-    default:
+    case 'none':
       return null
+    default: {
+      const _exhaustive: never = strategy
+      void _exhaustive
+      return null
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/swap-service/src/utils/affiliateFeeAsset.ts` around lines 27 - 41, The
function resolveAffiliateFeeAssetId currently checks `if (!strategy) return
null` which is now unreachable because `SWAPPER_FEE_STRATEGY` is a
Record<SwapperName, FeeAssetStrategy>; remove that guard, add an explicit case
for the 'none' strategy (returning null) in the switch, and replace the
`default` branch with a `const _exhaustiveCheck: never = strategy;` style
exhaustiveness assertion (or call an assertNever(strategy)) so TypeScript will
error if FeeAssetStrategy gains new members; reference
resolveAffiliateFeeAssetId, SWAPPER_FEE_STRATEGY, SwapperName and
FeeAssetStrategy when making these changes.
apps/swap-service/src/verification/swap-verification.service.ts (1)

137-141: Replace inline literals with class constants for the NearIntents referral.

The hardcoded 'shapeshift' (Line 137) and the single-element list ['shapeshifttokenomics.sputnik-dao.near'] (Line 140) break the pattern used elsewhere in this service (shapeshiftRelayReferrer, shapeshiftMayaAffiliate, etc.). Promoting them to private readonly fields keeps Near consistent with the other verifiers, makes future updates a one-line change, and avoids implicit case sensitivity on the recipient match.

♻️ Proposed refactor
+  private readonly shapeshiftNearReferral = 'shapeshift'
+  private readonly shapeshiftNearFeeRecipients = ['shapeshifttokenomics.sputnik-dao.near']
   ...
-    const hasShapeshiftReferral = referral?.toLowerCase() === 'shapeshift'
+    const hasShapeshiftReferral = referral?.toLowerCase() === this.shapeshiftNearReferral

     const shapeshiftFee = hasShapeshiftReferral
-      ? appFees.find(({ recipient }) => ['shapeshifttokenomics.sputnik-dao.near'].includes(recipient))
+      ? appFees.find(({ recipient }) => this.shapeshiftNearFeeRecipients.includes(recipient?.toLowerCase()))
       : undefined

Note: NEAR account IDs are case-insensitive in the protocol (canonical form is lowercase), so an explicit toLowerCase() on recipient defends against any upstream casing drift.

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

In `@apps/swap-service/src/verification/swap-verification.service.ts` around lines
137 - 141, Replace the inline literals in the swap verification logic with class
constants: add private readonly fields (e.g., shapeshiftReferral = 'shapeshift'
and shapeshiftTokenomicsRecipient = 'shapeshifttokenomics.sputnik-dao.near')
alongside the existing shapeshiftRelayReferrer/shapeshiftMayaAffiliate, then
update the two usages in the method that define hasShapeshiftReferral and
shapeshiftFee to reference those fields; also normalize recipient before
matching (recipient?.toLowerCase()) so the appFees.find uses the constant
comparison defensively.
🤖 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/shared-types/src/index.ts`:
- Line 73: Request bodies can contain arbitrary strings for swapperName because
CreateSwapDto is only an interface and there's no global ValidationPipe; this
allows invalid values to reach resolveAffiliateFeeAssetId and the swappers
lookup (swaps.service.ts) and bypass the exhaustive switch in
swap-verification.service.ts. Fix by converting CreateSwapDto to a class and add
`@IsEnum`(SwapperName) on the swapperName property (or apply a per-endpoint
validation pipe that checks SwapperName) and enable validation at the controller
boundary (or enable global ValidationPipe as noted by the TODO in main.ts) so
incoming requests are rejected for invalid SwapperName values before they reach
resolveAffiliateFeeAssetId or the swappers[...] lookup.

---

Outside diff comments:
In `@apps/swap-service/src/verification/swap-verification.service.ts`:
- Around line 220-228: verifyRelay, verifyChainflip and verifyZrx currently
return success results directly and skip the unified log line; update each
verifier to call logVerification(...) with the final result object (same fields
you return: isVerified, hasAffiliate, affiliateBps, affiliateAddress,
verifiedSellAmountCryptoBaseUnit, actualBuyAmountCryptoBaseUnit,
actualAffiliateFeeAmountCryptoBaseUnit) before returning it so the
pipe-separated stable format is emitted for these high-volume verifiers; locate
calls in functions verifyRelay, verifyChainflip and verifyZrx and invoke
logVerification(contextOrParams, result) (matching the existing logVerification
signature used elsewhere) and then return the logged result.

---

Nitpick comments:
In `@apps/swap-service/src/utils/affiliateFeeAsset.ts`:
- Around line 27-41: The function resolveAffiliateFeeAssetId currently checks
`if (!strategy) return null` which is now unreachable because
`SWAPPER_FEE_STRATEGY` is a Record<SwapperName, FeeAssetStrategy>; remove that
guard, add an explicit case for the 'none' strategy (returning null) in the
switch, and replace the `default` branch with a `const _exhaustiveCheck: never =
strategy;` style exhaustiveness assertion (or call an assertNever(strategy)) so
TypeScript will error if FeeAssetStrategy gains new members; reference
resolveAffiliateFeeAssetId, SWAPPER_FEE_STRATEGY, SwapperName and
FeeAssetStrategy when making these changes.

In `@apps/swap-service/src/verification/swap-verification.service.ts`:
- Around line 137-141: Replace the inline literals in the swap verification
logic with class constants: add private readonly fields (e.g.,
shapeshiftReferral = 'shapeshift' and shapeshiftTokenomicsRecipient =
'shapeshifttokenomics.sputnik-dao.near') alongside the existing
shapeshiftRelayReferrer/shapeshiftMayaAffiliate, then update the two usages in
the method that define hasShapeshiftReferral and shapeshiftFee to reference
those fields; also normalize recipient before matching
(recipient?.toLowerCase()) so the appFees.find uses the constant comparison
defensively.

In `@apps/swap-service/src/verification/utils.ts`:
- Around line 16-42: The logVerification helper can produce duplicate keys if
callers include extras that collide with base segments; update logVerification
to defensively drop or rename conflicting keys in the incoming extra object
before building segments: define a reserved set (e.g.,
'swapId','sell','buy','fee','affiliate' and any other base keys used in
segments), then filter Object.entries(extra) to exclude any key in that reserved
set (or alternatively prefix remaining keys), and finally build the extras array
from the filtered entries so no duplicate keys appear in the joined log; keep
the function name logVerification and the same output format.
🪄 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: c6edfedb-e313-441f-a908-225334de3d48

📥 Commits

Reviewing files that changed from the base of the PR and between f7ee048 and 9d322f8.

📒 Files selected for processing (6)
  • apps/swap-service/src/swaps/swaps.service.ts
  • apps/swap-service/src/swaps/types.ts
  • apps/swap-service/src/utils/affiliateFeeAsset.ts
  • apps/swap-service/src/verification/swap-verification.service.ts
  • apps/swap-service/src/verification/utils.ts
  • packages/shared-types/src/index.ts
💤 Files with no reviewable changes (1)
  • apps/swap-service/src/swaps/types.ts

Comment thread packages/shared-types/src/index.ts
@kaladinlight
kaladinlight force-pushed the refactor/swap-verification-contract branch from 9d322f8 to f8919bb Compare April 27, 2026 22:14
@kaladinlight
kaladinlight merged commit 3e83738 into develop Apr 27, 2026
1 check passed
@kaladinlight
kaladinlight deleted the refactor/swap-verification-contract branch May 6, 2026 21:58
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.

1 participant