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
23 changes: 12 additions & 11 deletions apps/swap-service/src/affiliate/affiliate.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import {
Req,
UseGuards,
} from '@nestjs/common'
import type { Affiliate } from '@prisma/client'

import { SHAPESHIFT_BPS } from '../swaps/constants'

import { AffiliateService } from './affiliate.service'
import { SiweAuthGuard, SiweRequest } from './siwe-auth.guard'
import {
AddressQueryDto,
AffiliateStatsQueryDto,
AffiliateSwapsQueryDto,
ClaimPartnerCodeDto,
Expand All @@ -25,6 +27,11 @@ import {
} from './types'
import { assertSiweMatches } from './utils'

const toAffiliateResponse = (affiliate: Affiliate) => {
const { bps: partnerBps, ...rest } = affiliate
return { ...rest, partnerBps, shapeshiftBps: SHAPESHIFT_BPS }
}

@Controller('v1/affiliate')
export class AffiliateController {
constructor(private affiliateService: AffiliateService) {}
Expand All @@ -39,17 +46,11 @@ export class AffiliateController {
return this.affiliateService.getAffiliateStats(query.address, query)
}

@Get('lookup/bps')
async lookupBps(@Query() query: AddressQueryDto) {
const bps = await this.affiliateService.lookupAffiliateBps(query.address)
return { bps }
}

@Get(':address')
async getAffiliate(@Param('address') address: string) {
const affiliate = await this.affiliateService.getAffiliateByWalletAddress(address)
if (!affiliate) throw new NotFoundException('Affiliate not found')
return affiliate
return toAffiliateResponse(affiliate)
}

@UseGuards(SiweAuthGuard)
Expand All @@ -58,7 +59,7 @@ export class AffiliateController {
assertSiweMatches(req, data.walletAddress, 'Authenticated address does not match walletAddress')

try {
return await this.affiliateService.createAffiliate(data)
return toAffiliateResponse(await this.affiliateService.createAffiliate(data))
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('already')) throw new ConflictException(error.message)
Expand All @@ -72,7 +73,7 @@ export class AffiliateController {
async updateAffiliate(@Req() req: SiweRequest, @Param('address') address: string, @Body() data: UpdateAffiliateDto) {
assertSiweMatches(req, address, 'Authenticated address does not match target address')

return this.affiliateService.updateAffiliate(address, data)
return toAffiliateResponse(await this.affiliateService.updateAffiliate(address, data))
}

@UseGuards(SiweAuthGuard)
Expand All @@ -81,7 +82,7 @@ export class AffiliateController {
assertSiweMatches(req, data.walletAddress, 'Authenticated address does not match walletAddress')

try {
return await this.affiliateService.claimPartnerCode(data.walletAddress, data.partnerCode)
return toAffiliateResponse(await this.affiliateService.claimPartnerCode(data.walletAddress, data.partnerCode))
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('taken') || error.message.includes('reserved')) {
Expand Down
23 changes: 10 additions & 13 deletions apps/swap-service/src/affiliate/affiliate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Injectable, NotFoundException } from '@nestjs/common'
import { Affiliate, Prisma } from '@prisma/client'

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

import type {
Expand Down Expand Up @@ -76,12 +77,12 @@ export class AffiliateService {
})
}

async getAffiliateStats(affiliateAddress: string, options: AffiliateStatsQueryDto): Promise<AffiliateStatsResult> {
async getAffiliateStats(address: string, options: AffiliateStatsQueryDto): Promise<AffiliateStatsResult> {
const { startDate, endDate } = options

const items = await this.prisma.swap.findMany({
where: {
affiliateAddress,
partnerAddress: address,
status: 'SUCCESS',
isAffiliateVerified: true,
...(startDate || endDate
Expand All @@ -105,7 +106,7 @@ export class AffiliateService {
const fee = calculateFeeForSwap(swap)
if (!fee) continue

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

totalSwaps++
totalVolumeUsd += fee.volumeUsd
Expand All @@ -119,13 +120,13 @@ export class AffiliateService {
}
}

async getAffiliateSwaps(affiliateAddress: string, options: AffiliateSwapsQueryDto): Promise<PaginatedSwaps> {
async getAffiliateSwaps(address: string, options: AffiliateSwapsQueryDto): Promise<PaginatedSwaps> {
const { startDate, endDate, limit, cursor } = options

const items = await this.prisma.swap.findMany({
...swapCursorArgs(limit, cursor),
where: {
affiliateAddress,
partnerAddress: address,
...(startDate || endDate
? {
createdAt: {
Expand All @@ -148,14 +149,10 @@ export class AffiliateService {
if (!affiliate) return null

return {
partnerBps: affiliate.bps,
partnerCode: affiliate.partnerCode,
affiliateAddress: affiliate.receiveAddress ?? affiliate.walletAddress,
bps: affiliate.bps,
partnerAddress: affiliate.receiveAddress ?? affiliate.walletAddress,
shapeshiftBps: SHAPESHIFT_BPS,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

async lookupAffiliateBps(affiliateAddress: string): Promise<number> {
const affiliate = await this.getAffiliateByWalletAddress(affiliateAddress)
return affiliate?.bps ?? 60
}
}
2 changes: 2 additions & 0 deletions apps/swap-service/src/swaps/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const SHAPESHIFT_BPS = 10
export const REFERRER_FEE_RATE = 0.1
6 changes: 3 additions & 3 deletions apps/swap-service/src/swaps/swaps.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ export class SwapsController {
return this.swapsService.calculateReferralFees(referralCode, startDate, endDate)
}

@Get('affiliate-fees/:affiliateAddress')
@Get('affiliate-fees/:address')
async getAffiliateFees(
@Param('affiliateAddress') affiliateAddress: string,
@Param('address') address: string,
@Query('startDate', OptionalDatePipe) startDate?: Date,
@Query('endDate', OptionalDatePipe) endDate?: Date,
) {
return this.swapsService.calculateAffiliateFees(affiliateAddress, startDate, endDate)
return this.swapsService.calculateAffiliateFees(address, startDate, endDate)
}
}
47 changes: 22 additions & 25 deletions apps/swap-service/src/swaps/swaps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { Prisma } from '@prisma/client'

import { CreateSwapDto, Fees, SwapStatusResponse, UpdateSwapStatusDto } from '@shapeshift/shared-types'
import { hashAccountId, NotificationsServiceClient, UserServiceClient } from '@shapeshift/shared-utils'
import { NotificationsServiceClient, UserServiceClient } from '@shapeshift/shared-utils'
import { swappers } from '@shapeshiftoss/swapper'
import { TxStatus } from '@shapeshiftoss/unchained-client'

Expand All @@ -26,6 +26,7 @@ import { resolveAffiliateFeeAssetId } from '../utils/affiliateFeeAsset'
import { getNextCursor, swapCursorArgs } from '../utils/pagination'
import { SwapVerificationService } from '../verification/swap-verification.service'

import { REFERRER_FEE_RATE } from './constants'
import { buildChainAdapterAsserts, getSwapperConfig } from './swapper-config'
import type { AffiliateVerificationDetails, AggregateFeesParams, FeeTotals, PaginatedSwaps, Swap } from './types'
import { PaginationQueryDto } from './types'
Expand All @@ -34,7 +35,7 @@ import {
calculateFeeForSwap,
computeSellAmountUsd,
fetchUsdPrices,
getAffiliateFeeRate,
getPartnerFeeRate,
toSwap,
toSwapperSwap,
} from './utils'
Expand All @@ -47,9 +48,6 @@ export class SwapsService {
private readonly userServiceClient: UserServiceClient
private readonly chainAdapterAsserts: ReturnType<typeof buildChainAdapterAsserts>

private static readonly API_BASE_BPS = 10
private static readonly REFERRER_FEE_RATE = 0.1

constructor(
private prisma: PrismaService,
private swapVerificationService: SwapVerificationService,
Expand Down Expand Up @@ -82,10 +80,10 @@ export class SwapsService {
try {
const affiliateFeeAssetId = resolveAffiliateFeeAssetId(data.swapperName, data.sellAsset, data.buyAsset)

const [referralCode, prices, affiliateAddress] = await Promise.all([
const [referralCode, prices, partnerAddress] = await Promise.all([
this.getReferralCode(data.userId),
fetchUsdPrices(data, affiliateFeeAssetId),
this.resolveAffiliateAddress(data),
this.resolvePartnerAddress(data),
])

const sellAmountUsd = computeSellAmountUsd(
Expand All @@ -107,8 +105,8 @@ export class SwapsService {
expectedBuyAmountCryptoBaseUnit: data.expectedBuyAmountCryptoBaseUnit,
source: data.source,
swapperName: data.swapperName,
sellAccountId: data.sellAccountId ? hashAccountId(data.sellAccountId) : 'api',
buyAccountId: data.buyAccountId ? hashAccountId(data.buyAccountId) : null,
sellAccountId: data.sellAccountId,
buyAccountId: data.buyAccountId,
receiveAddress: data.receiveAddress,
isStreaming: data.isStreaming ?? false,
metadata: (data.metadata ?? {}) as Prisma.InputJsonValue,
Expand All @@ -117,10 +115,11 @@ export class SwapsService {
sellAssetUsd: prices.sellAssetUsd,
buyAssetUsd: prices.buyAssetUsd,
affiliateAssetUsd: prices.affiliateAssetUsd,
affiliateAddress,
affiliateBps: data.affiliateBps ?? null,
partnerAddress,
partnerBps: data.partnerBps,
affiliateBps: data.affiliateBps,
shapeshiftBps: data.shapeshiftBps,
origin: data.origin ?? null,
shapeshiftBps: SwapsService.API_BASE_BPS,
affiliateFeeAssetId,
},
}),
Expand All @@ -130,7 +129,7 @@ export class SwapsService {
[
`Swap created: ${swap.swapId}`,
referralCode && `referral ${referralCode}`,
affiliateAddress && `affiliate ${affiliateAddress}`,
partnerAddress && `partner ${partnerAddress}`,
sellAmountUsd && `$${sellAmountUsd}`,
]
.filter(Boolean)
Expand All @@ -149,8 +148,8 @@ export class SwapsService {
return this.userServiceClient.getUserReferralCode(userId)
}

private async resolveAffiliateAddress(data: CreateSwapDto): Promise<string | null> {
if (data.affiliateAddress) return data.affiliateAddress
private async resolvePartnerAddress(data: CreateSwapDto): Promise<string | null> {
if (data.partnerAddress) return data.partnerAddress
if (!data.partnerCode) return null

try {
Expand All @@ -161,7 +160,7 @@ export class SwapsService {

return affiliate?.receiveAddress ?? affiliate?.walletAddress ?? null
} catch (error) {
logger.warn(`Failed to resolve affiliate address for partner code ${data.partnerCode}:`, error)
logger.warn(`Failed to resolve partner address for partner code ${data.partnerCode}:`, error)
return null
}
}
Expand Down Expand Up @@ -219,9 +218,7 @@ export class SwapsService {
}

async getSwapsByAccountId(accountId: string, options: PaginationQueryDto): Promise<PaginatedSwaps> {
const hashedAccountId = hashAccountId(accountId)

return this.paginateSwaps({ OR: [{ sellAccountId: hashedAccountId }, { buyAccountId: hashedAccountId }] }, options)
return this.paginateSwaps({ OR: [{ sellAccountId: accountId }, { buyAccountId: accountId }] }, options)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private async paginateSwaps(where: Prisma.SwapWhereInput, options: PaginationQueryDto): Promise<PaginatedSwaps> {
Expand Down Expand Up @@ -267,7 +264,7 @@ export class SwapsService {
calcFee: (swap) => {
const fee = calculateFeeForSwap(swap)
if (!fee) return null
return { feeUsd: fee.feeUsd * SwapsService.REFERRER_FEE_RATE, volumeUsd: fee.volumeUsd }
return { feeUsd: fee.feeUsd * REFERRER_FEE_RATE, volumeUsd: fee.volumeUsd }
},
})

Expand All @@ -287,25 +284,25 @@ export class SwapsService {
}
}

async calculateAffiliateFees(affiliateAddress: string, startDate?: Date, endDate?: Date): Promise<Fees> {
async calculateAffiliateFees(address: string, startDate?: Date, endDate?: Date): Promise<Fees> {
logger.log(
`Calculating affiliate fees for address: ${affiliateAddress}, period: ${startDate?.toISOString()} - ${endDate?.toISOString()}`,
`Calculating affiliate fees for address: ${address}, period: ${startDate?.toISOString()} - ${endDate?.toISOString()}`,
)

const fees = await this.aggregateFees({
baseWhere: { affiliateAddress, isAffiliateVerified: true, status: 'SUCCESS', origin: 'api' },
baseWhere: { partnerAddress: address, isAffiliateVerified: true, status: 'SUCCESS', origin: 'api' },
startDate,
endDate,
calcFee: (swap) => {
const fee = calculateFeeForSwap(swap)
if (!fee) return null
const rate = getAffiliateFeeRate(fee.verifiedBps, swap.shapeshiftBps)
const rate = getPartnerFeeRate(fee.verifiedBps, swap.partnerBps)
return { feeUsd: fee.feeUsd * rate, volumeUsd: fee.volumeUsd }
},
})

logger.log(
`Affiliate fees for ${affiliateAddress}\n` +
`Affiliate fees for ${address}\n` +
` period: ${fees.periodCount} swaps, $${fees.periodVolumeUsd.toFixed(2)} volume, $${fees.periodFeesUsd.toFixed(2)} fee\n` +
` all-time: ${fees.allTimeCount} swaps, $${fees.allTimeFeesUsd.toFixed(2)} fee`,
)
Expand Down
8 changes: 3 additions & 5 deletions apps/swap-service/src/swaps/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@ export const formatAmount = (amount: string | number): string => {
.replace(/\.?0+$/, '')
}

export const getAffiliateFeeRate = (verifiedBps: number, shapeshiftBps: number): number => {
// Partner configured at or below the platform floor — partner keeps the whole fee.
if (verifiedBps <= shapeshiftBps) return 1
// Platform takes shapeshiftBps; partner gets the remainder.
return (verifiedBps - shapeshiftBps) / verifiedBps
export const getPartnerFeeRate = (verifiedBps: number, partnerBps: number): number => {
if (verifiedBps <= 0) return 0
return Math.min(partnerBps / verifiedBps, 1)
}

export const computeSellAmountUsd = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,7 @@
"txID": ""
}
],
"pools": [
"ETH.ETH",
"ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48"
],
"pools": ["ETH.ETH", "ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48"],
"status": "success",
"type": "swap"
}
Expand All @@ -91,4 +88,4 @@
"nextPageToken": "165627049000000001",
"prevPageToken": "165627049000000001"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export default {
status: 'SUCCESS',
source: 'MAYAChain',
swapperName: SwapperName.Mayachain,
sellAccountId: '3c70e97c6f86a5b5cfdf82dfd3380ac4ed3d8b89dabf86f631bdf739372926be',
buyAccountId: null,
sellAccountId: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
buyAccountId: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
receiveAddress: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
sellTxHash: '0xe4dac04d7194aba8f3a9fa154cd7be3055a03d35797f1916d8721f6273fa16eb',
buyTxHash: '0x389BD007E29E081811C735F773D360A05DFC3E0C8608781A85602A7C7AE2AD4B',
Expand All @@ -72,8 +72,9 @@ export default {
affiliateAssetUsd: '2309.11',
isAffiliateVerified: null,
affiliateVerificationDetails: null,
affiliateAddress: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
partnerAddress: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
affiliateBps: 60,
partnerBps: 50,
origin: 'api',
affiliateFeeAssetId: 'eip155:1/slip44:60',
actualAffiliateFeeAmountCryptoBaseUnit: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export default {
status: 'PENDING',
source: 'NEAR Intents',
swapperName: SwapperName.NearIntents,
sellAccountId: '3c70e97c6f86a5b5cfdf82dfd3380ac4ed3d8b89dabf86f631bdf739372926be',
buyAccountId: null,
sellAccountId: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
buyAccountId: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
receiveAddress: '0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
sellTxHash: '0x134662bb1608962d8c4c6cf4b32bc7a10fefa282ced805b90c04be809ee93310',
buyTxHash: null,
Expand Down Expand Up @@ -72,8 +72,9 @@ export default {
isAffiliateVerified: null,
affiliateVerificationDetails: null,
verificationStatus: 'PENDING',
affiliateAddress: '0xa44c286ba83bb771cd0107b2c1df678435bd1535',
partnerAddress: '0xa44c286ba83bb771cd0107b2c1df678435bd1535',
affiliateBps: 60,
partnerBps: 50,
origin: 'api',
affiliateFeeAssetId: 'eip155:1/slip44:60',
actualAffiliateFeeAmountCryptoBaseUnit: null,
Expand Down
Loading
Loading