Skip to content

feat(swap-service): Thorchain swap verification#38

Merged
kaladinlight merged 7 commits into
developfrom
feat/swap-thorchain-verification
May 9, 2026
Merged

feat(swap-service): Thorchain swap verification#38
kaladinlight merged 7 commits into
developfrom
feat/swap-thorchain-verification

Conversation

@kaladinlight
Copy link
Copy Markdown
Member

Description

Implements ShapeShift affiliate verification for THORChain swaps via Midgard's /actions endpoint, then hardens the result against malformed/foreign data so we don't pollute settlement records.

  • Verifier (swap-verification.service.ts verifyThorchain): fetch the action by inbound txid, validate type === 'swap', treat status === 'failed' as FAILED (was silently producing a SUCCESS), and return PENDING while still in flight.
  • Buy out selection by memo destination: parse the destination address from the on-chain memo (=:ASSET:DEST:LIM/...:AFFILIATE:FEE) and match by !affiliate && address === dest, instead of relying on out[length-1] array position.
  • Affiliate gating: hasAffiliate requires both affiliateAddress === 'ss' AND a real affiliate out — neither alone is sufficient. When hasAffiliate is false, affiliateBps, affiliateAddress, and actualAffiliateFeeAmountCryptoBaseUnit are all undefined so foreign-affiliate or no-fee swaps don't write attribution data.
  • Affiliate fee asset: THORChain pays affiliates in RUNE regardless of swap legs; affiliateFeeAsset.ts now supports a fixed AssetId strategy and uses thorchainAssetId for THORChain (was incorrectly sell_asset).
  • Test setup: stub @shapeshiftoss/chain-adapters in jest setup (it transitively imports the ESM-only p-queue which Jest can't parse) and add the missing VITE_THORCHAIN_MIDGARD_URL env. Replace the two old synthetic fixtures with a real captured Midgard /actions response.

Testing

  • yarn test (in apps/swap-service) — all suites green, including new Thorchain cases:
    • successful SS-affiliate swap maps inbound/outbound coins to native precision correctly
    • 0x prefix is stripped from sellTxHash before the Midgard call
    • non-ss affiliate ⇒ hasAffiliate=false and all affiliate fields undefined
    • ss memo but no affiliate out ⇒ hasAffiliate=false
    • buy out is selected by memo destination, not array order
    • no out matching memo destination ⇒ FAILED
    • empty/unparseable memo ⇒ FAILED
    • status === 'failed'FAILED
    • missing sellTxHashFAILED; HTTP error ⇒ PENDING
  • Run a real ETH→ERC20 Thorchain swap with ShapeShift affiliate and confirm the resulting swap_verification row records RUNE as the affiliate fee asset and the destination-matched buy amount.

🤖 Generated with Claude Code

kaladinlight and others added 6 commits May 5, 2026 17:54
WIP commit moved to its own branch so the swap verification status
refactor (verificationStatus column + decoupled polling) can land
independently on develop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	apps/swap-service/src/verification/swap-verification.service.ts
#	apps/swap-service/src/verification/utils.ts
@shapeshiftoss/chain-adapters transitively imports p-queue (ESM-only),
which Jest's CJS loader can't parse. The verification tests only need
bnOrZero, so stub the package via bignumber.js. Also add the missing
VITE_THORCHAIN_MIDGARD_URL env stub used by the Thorchain verifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the two synthetic Thornode fixtures (status/tx responses) with
a single Midgard /actions response captured from a real ETH→USDC swap
with ShapeShift affiliate, and update the matching Swap row fixture so
verifier assertions reflect on-chain values rather than hand-rolled
shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Treat action.status === 'failed' as FAILED (was falling through to
  the success path and producing a bogus SUCCESS verification).
- Select the buy out by matching the memo destination address (the
  on-chain trusted value) instead of relying on array position.
- Require both affiliateAddress === 'ss' AND a real affiliate out for
  hasAffiliate=true; gate affiliateBps, affiliateAddress, and
  actualAffiliateFeeAmountCryptoBaseUnit on that flag so foreign
  affiliate data never lands in our settlement record.
- Treat "no out matching memo destination" as FAILED rather than
  PENDING — by Midgard invariants, post-pending actions have fully
  populated outs, so a mismatch is definitive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thorchain settles affiliate fees in RUNE regardless of the swap legs,
so the previous 'sell_asset' strategy was wrong. Generalize the fee
strategy to allow a fixed AssetId (RUNE here) and use null in place
of the sentinel string for swappers with no affiliate fee asset.

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

coderabbitai Bot commented May 8, 2026

Warning

Rate limit exceeded

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

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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: 04ac2c05-8b47-45d1-ba22-fa503d3d75bc

📥 Commits

Reviewing files that changed from the base of the PR and between f9efb3e and 6789926.

📒 Files selected for processing (8)
  • apps/swap-service/src/utils/affiliateFeeAsset.ts
  • apps/swap-service/src/verification/__tests__/fixtures/thorchain/response.json
  • apps/swap-service/src/verification/__tests__/fixtures/thorchain/swap.ts
  • apps/swap-service/src/verification/__tests__/setup.ts
  • apps/swap-service/src/verification/__tests__/thorchain.test.ts
  • apps/swap-service/src/verification/swap-verification.service.ts
  • apps/swap-service/src/verification/types.ts
  • apps/swap-service/src/verification/utils.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/swap-thorchain-verification

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.

Use jest.requireActual with a top-level type-only import for the
BigNumber type, replacing bare require() and inline import() type
annotations that tripped @typescript-eslint/no-require-imports and
consistent-type-imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight merged commit c0c026c into develop May 9, 2026
2 checks passed
@kaladinlight kaladinlight deleted the feat/swap-thorchain-verification branch May 9, 2026 03:47
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