Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/swap-service/src/affiliate/affiliate.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Injectable, NotFoundException } from '@nestjs/common'
import { Affiliate, Prisma } from '@prisma/client'

import { bnOrZero } from '@shapeshiftoss/chain-adapters'

import { PrismaService } from '../prisma/prisma.service'
import { SHAPESHIFT_BPS } from '../swaps/constants'
import { calculateFeeForSwap, getPartnerFeeRate, toSwap } from '../swaps/utils'
import { calculateFeeForSwap, getPartnerFeeUsd, toSwap } from '../swaps/utils'
import { getNextCursor, swapCursorArgs } from '../utils/pagination'

import type { AffiliateStatsResult, CreateAffiliateDto, UpdateAffiliateDto } from './types'
Expand Down Expand Up @@ -84,20 +86,18 @@ export class AffiliateService {
})

let totalSwaps = 0
let totalVolumeUsd = 0
let totalFeesEarnedUsd = 0
let totalVolumeUsd = bnOrZero(0)
let totalFeesEarnedUsd = bnOrZero(0)

for (const item of items) {
const swap = toSwap(item)

const fee = calculateFeeForSwap(swap)
if (!fee) continue

const rate = getPartnerFeeRate(fee.verifiedBps, swap.partnerBps)

totalSwaps++
totalVolumeUsd += fee.volumeUsd
totalFeesEarnedUsd += fee.feeUsd * rate
totalVolumeUsd = totalVolumeUsd.plus(fee.volumeUsd)
totalFeesEarnedUsd = totalFeesEarnedUsd.plus(getPartnerFeeUsd(fee.feeUsd, fee.verifiedBps, swap.partnerBps))
}

return {
Expand Down Expand Up @@ -133,7 +133,7 @@ export class AffiliateService {
const fee = calculateFeeForSwap(swap)
const feeUsd = fee ? fee.feeUsd.toString() : null
const volumeUsd = fee ? fee.volumeUsd.toString() : null
const partnerFeeUsd = fee ? (fee.feeUsd * getPartnerFeeRate(fee.verifiedBps, swap.partnerBps)).toString() : null
const partnerFeeUsd = fee ? getPartnerFeeUsd(fee.feeUsd, fee.verifiedBps, swap.partnerBps) : null
return { ...swap, feeUsd, partnerFeeUsd, volumeUsd }
})

Expand Down
10 changes: 7 additions & 3 deletions apps/swap-service/src/swaps/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ export const formatAmount = (amount: string | number): string => {
.replace(/\.?0+$/, '')
}

export const getPartnerFeeRate = (verifiedBps: number, partnerBps: number): number => {
if (verifiedBps <= 0) return 0
return Math.min(partnerBps / verifiedBps, 1)
// The partner's share of the affiliate fee in USD, as an exact string. Multiplies before dividing
// (feeUsd × partnerBps ÷ verifiedBps) so the result stays precise — computing a partnerBps/verifiedBps
// rate first (e.g. 50/60) would introduce lossy-float artifacts. Capped at the whole fee.
export const getPartnerFeeUsd = (feeUsd: number, verifiedBps: number, partnerBps: number): string => {
if (verifiedBps <= 0) return '0'
const share = bnOrZero(feeUsd).times(partnerBps).div(verifiedBps)
return (share.gt(feeUsd) ? bnOrZero(feeUsd) : share).toString()
}

export const computeSellAmountUsd = (
Expand Down
3 changes: 2 additions & 1 deletion scripts/affiliate-payouts/affiliate-payouts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ const stubDeps: FeeDeps<PrismaSwap> = {
if (r.priceable === false) return null
return { feeUsd: 12, volumeUsd: 2000, verifiedBps: 60, actualFeeUsd: 12, impliedFeeUsd: 12, ...r.fee }
},
getPartnerFeeRate: (verifiedBps, partnerBps) => (verifiedBps <= 0 ? 0 : Math.min(partnerBps / verifiedBps, 1)),
getPartnerFeeUsd: (feeUsd, verifiedBps, partnerBps) =>
verifiedBps <= 0 ? '0' : BigNumber.min(new BigNumber(feeUsd).times(partnerBps).div(verifiedBps), feeUsd).toString(),
}

const accrual = (over: Partial<PartnerAccrual> & Pick<PartnerAccrual, 'partnerCode'>): PartnerAccrual => ({
Expand Down
4 changes: 2 additions & 2 deletions scripts/affiliate-payouts/affiliate-payouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PrismaClient } from '@prisma/client'
import * as fs from 'fs'
import * as path from 'path'

import { calculateFeeForSwap, getPartnerFeeRate, toSwap } from '../../apps/swap-service/src/swaps/utils'
import { calculateFeeForSwap, getPartnerFeeUsd, toSwap } from '../../apps/swap-service/src/swaps/utils'

import type { PartnerPayout, PayoutRecord } from './types'
import { aggregateByPartner, buildPayouts, buildRecord, resolveWindow, toCsv } from './utils'
Expand Down Expand Up @@ -88,7 +88,7 @@ async function generate(monthArg: string | undefined, force: boolean): Promise<v
console.log(`Found ${rows.length} successful swaps with a partner code`)

const { partners, unpriceableSwaps, anomalies, unverified, noAffiliateFee, partnerBpsUnset, unresolvedFee } =
aggregateByPartner(rows, { toSwap, calculateFeeForSwap, getPartnerFeeRate })
aggregateByPartner(rows, { toSwap, calculateFeeForSwap, getPartnerFeeUsd })

const affiliates = await prisma.affiliate.findMany({
where: { partnerCode: { in: Array.from(partners.keys()) } },
Expand Down
2 changes: 1 addition & 1 deletion scripts/affiliate-payouts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export type AggregateResult = {
export type FeeDeps<S> = {
toSwap: (row: PrismaSwap) => S
calculateFeeForSwap: (swap: S) => FeeResult | null
getPartnerFeeRate: (verifiedBps: number, partnerBps: number) => number
getPartnerFeeUsd: (feeUsd: number, verifiedBps: number, partnerBps: number) => string
}

export type PayoutWarning = {
Expand Down
6 changes: 3 additions & 3 deletions scripts/affiliate-payouts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ export function aggregateByPartner<S>(
continue
}

const rate = deps.getPartnerFeeRate(fee.verifiedBps, row.partnerBps)
if (rate <= 0) {
if (row.partnerBps <= 0) {
partnerBpsUnset.push({
swapId: row.swapId,
partnerCode,
Expand All @@ -159,9 +158,10 @@ export function aggregateByPartner<S>(
feesEarnedUsd: new BigNumber(0),
}

// Pay only on the verified on-chain fee, via the shared exact partner-share helper.
accrual.swapCount += 1
accrual.volumeUsd = accrual.volumeUsd.plus(fee.volumeUsd)
accrual.feesEarnedUsd = accrual.feesEarnedUsd.plus(new BigNumber(fee.actualFeeUsd).times(rate))
accrual.feesEarnedUsd = accrual.feesEarnedUsd.plus(deps.getPartnerFeeUsd(fee.actualFeeUsd, fee.verifiedBps, row.partnerBps))

partners.set(partnerCode.toLowerCase(), accrual)
}
Expand Down
Loading