Skip to content

[Swap] Add VULT-scaled affiliate fee to Android Jupiter + route native SOL via THORChain #5053

Description

@realpaaao

[Swap] Add VULT-scaled affiliate fee to Android Jupiter + route native SOL via THORChain

See 00-shared-spec.md for the cross-platform routing/fee spec.

Context

Android already uses Jupiter as its Solana swap provider (and currently the only
Solana route), but with zero affiliate fee — the quote sends only
inputMint/outputMint/amount/slippageBps and the swap body has no feeAccount:

  • data/.../api/JupiterApi.ktgetSwapQuote() (no platformFeeBps, no feeAccount)
  • data/.../repositories/swap/JupiterQuoteSource.kt — quote source
  • app/.../ui/models/swap/SwapQuoteManager.kt:549-558providerPriority (Jupiter = lowest, Solana-only)

Every other provider charges a VULT-scaled 50 bps (GetDiscountBpsUseCase,
THORChainSwaps, KyberApi, OneInchApi, LiFiChainApi). We want Jupiter to match, and
native SOL cross-chain to prefer THORChain (consistency with iOS/SDK).

Scope

  1. JupiterApi.kt — add affiliate fee:
    • Add affiliateBps: Int? (and the output mint) to getSwapQuote(...).
    • Quote: parameter("platformFeeBps", affiliateBps) when > 0.
    • Swap body: add put("feeAccount", <ATA(feeOwner, outputMint)>).
  2. JupiterQuoteSource.kt — pass the VULT-scaled bps through. Compute via
    GetDiscountBpsUseCaseaffiliateBps = max(0, 50 − discountBps) (same as Kyber/1inch/LiFi).
  3. Routing (SwapQuoteManager.kt providerPriority):
    • Native SOL ↔ other chain: THORChain first (confirm THORChain SOL support is wired
      on Android; the SDK enables it), then SwapKit/LiFi.
    • SOL ↔ SPL / SPL ↔ SPL: Jupiter first, LiFi fallback.
    • SPL ↔ other chain: THORChain (if supported) → SwapKit → LiFi (Jupiter not eligible).
    • Keep Jupiter gated to same-chain Solana only.
  4. feeAccount derivation — ATA of (owner = fee wallet, mint = output mint). Use
    wallet-core / Solana SPL ATA derivation. Create-if-missing or pre-provision common mints.

Fee account

Fee owner wallet: 8iqhrtBzMcYLR6c6FkzeoMHibedYDkHvLKnX2ArNie5z.
Not using Jupiter's on-chain referral program (we keep 100% of the fee).

Files

  • data/src/main/kotlin/com/vultisig/wallet/data/api/JupiterApi.kt
  • data/src/main/kotlin/com/vultisig/wallet/data/repositories/swap/JupiterQuoteSource.kt
  • app/src/main/java/com/vultisig/wallet/ui/models/swap/SwapQuoteManager.kt
  • app/.../data/usecases/GetDiscountBpsUseCase.kt (reuse)

Tests

  • Jupiter quote includes platformFeeBps = VULT-scaled bps; swap includes feeAccount.
  • 0-discount user → 50 bps; tiered user → reduced; never negative.
  • Native SOL→BTC/ETH routes via THORChain, not Jupiter.
  • SOL↔SPL / SPL↔SPL still route via Jupiter.

Acceptance

See 00-shared-spec.md.


Shared spec (cross-platform)

Jupiter / Solana swap alignment — shared spec (read first)

This is the common spec referenced by all four platform issues. Goal: make Solana
swap routing identical across SDK (Windows/extension), iOS, and Android, route
on-Solana token swaps through Jupiter (no aggregator markup), keep native SOL
cross-chain on THORChain, and charge a VULT-scaled affiliate fee on Jupiter.

Routing rules (apply on every platform)

Swap shape Provider preference order
Native SOL ↔ other chain (cross-chain) THORChain → SwapKit → LiFi
SOL ↔ SPL or SPL ↔ SPL (stays on Solana) Jupiter → LiFi (fallback)
SPL token ↔ other chain (cross-chain) THORChain (if pair supported) → SwapKit → LiFi

Hard constraint: Jupiter is Solana-only and same-chain — it cannot do any
cross-chain route. Only offer Jupiter when from.chain == Solana && to.chain == Solana.

Affiliate fee on Jupiter

Jupiter the aggregator takes 0%; the only fee charged is the one we add.

  • Quote (GET .../swap/v1/quote): add platformFeeBps=<affiliateBps>.
  • Swap (POST .../swap/v1/swap): add feeAccount=<our fee ATA for the fee mint>.
  • Fee-mint constraint (ExactIn): the fee mint must be the input or output mint.
    Use the OUTPUT mint as the fee mint. (ExactOut would force the input mint —
    we run ExactIn, so output-mint is fine.)

Fee account

  • Fee owner wallet (Solana): 8iqhrtBzMcYLR6c6FkzeoMHibedYDkHvLKnX2ArNie5z
  • feeAccount = the Associated Token Account (ATA) of (owner = fee wallet, mint = output mint).
    Derive per output mint: getAssociatedTokenAddressSync(outputMint, feeOwner).
  • Provisioning decision: create-if-missing, per swap, per asset. Each swap prepends an
    idempotent create-ATA instruction (createAssociatedTokenAccountIdempotent) for the
    fee ATA (output mint, owner = fee wallet). No pre-provisioning. We are not using the
    Jupiter on-chain Referral Program (so Jupiter keeps 0% of our fee), just a self-owned ATA.
    • ⚠️ Jupiter does NOT auto-create the feeAccount ATA. Its /swap endpoint auto-creates
      the user's output ATA, but the fee account is a precondition you must supply already
      initialized. So we MUST prepend our own createAssociatedTokenAccountIdempotent for the
      fee ATA. There is no request flag for this. (Confirmed against Jupiter docs.)
    • ⚠️ Rent (~0.002 SOL) is paid by whoever signs the create instruction. Prepended to the
      user's swap tx → the user pays (first time per fee mint). If we'd rather absorb it,
      create the fee ATA in a separate Vultisig-funded tx (one-time per mint) instead of per
      swap. Default per this ticket = create-if-missing in the user's tx. Implementer should
      inspect the returned tx instructions to confirm the rent-payer.

Fee consolidation (USDC) — NOT possible via the fee mechanism

Jupiter requires the fee mint to be one of the swap-pair mints (input or output for
ExactIn). So you cannot point every swap's feeAccount at a single USDC account — for a
pair where neither leg is USDC (e.g. SOL→BONK) the fee can only land in SOL or BONK.
Therefore fees accrue across many mints (the output mint of each swap). If consolidating
to USDC is desired later, that's a separate periodic sweep/swap job, independent of the
swap fee mechanism — not a config flag. (Confirmed against Jupiter docs.)

Fee scaling by VULT holding (reuse existing logic — do NOT reinvent)

Every other provider already does: affiliateBps = max(0, baseBps(50) − vultTierDiscountBps).
Apply the same value to Jupiter's platformFeeBps.

  • VULT tiers / discount bps (SDK swap/affiliate/config.ts): base 50; tier discount
    Bronze 5 / Silver 10 / Gold 20 / Platinum 25 / Diamond 35 / Ultimate 50 (NFT = +1 tier).
  • SDK: getSwapAffiliateBps(vultDiscountTier).
  • Android: GetDiscountBpsUseCasemax(0, 50 − discount).
  • iOS: bps(for: vultTierDiscount) pattern (see LiFiService 141-142).

API base (already proxied — keep using the proxy)

  • Quote: https://api.vultisig.com/jup/swap/v1/quote
  • Swap: https://api.vultisig.com/jup/swap/v1/swap
  • Params: inputMint, outputMint, amount (raw atomic units, no decimals),
    slippageBps (default 50), platformFeeBps (new).
  • Swap body: quoteResponse, userPublicKey, compute-unit config, feeAccount (new).
  • NOTE: legacy quote-api.jup.ag/v6 was deprecated 2025-10-01; we already use swap/v1.

Acceptance (all platforms)

  • Native SOL cross-chain quotes come from THORChain first.
  • SOL↔SPL and SPL↔SPL quotes come from Jupiter, LiFi only as fallback.
  • Jupiter quote request includes platformFeeBps = VULT-scaled affiliate bps.
  • Jupiter swap request includes feeAccount = ATA(feeOwner, outputMint).
  • VULT holder sees reduced bps; 0-discount user sees 50 bps; never negative.
  • Cross-chain-from-Solana never selects Jupiter.
  • Behaviour matches the other two platforms for the same pair.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions