From 0f9d702fe81b4352b158e9a146f0c55f03e4615b Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:02:20 -0600 Subject: [PATCH 1/9] refactor(swap-service): tighten partnerCode to lowercase alphanumeric Aligns with the dashboard/public-api validation: lowercase a-z and digits only, 3-32 characters, no hyphens or mixed case. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/swap-service/src/affiliate/types.ts | 2 +- apps/swap-service/src/affiliate/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/swap-service/src/affiliate/types.ts b/apps/swap-service/src/affiliate/types.ts index 7e5f706..b3197cf 100644 --- a/apps/swap-service/src/affiliate/types.ts +++ b/apps/swap-service/src/affiliate/types.ts @@ -5,7 +5,7 @@ import { PaginationQueryDto } from '../swaps/types' import { PARTNER_CODE_REGEX } from './utils' -const PARTNER_CODE_MESSAGE = 'Partner code must be 3-32 alphanumeric characters or hyphens' +const PARTNER_CODE_MESSAGE = 'Partner code must be 3-32 lowercase letters or numbers (e.g. mypartnercode)' export interface AffiliateStatsResult { totalSwaps: number diff --git a/apps/swap-service/src/affiliate/utils.ts b/apps/swap-service/src/affiliate/utils.ts index ea60664..eb94a59 100644 --- a/apps/swap-service/src/affiliate/utils.ts +++ b/apps/swap-service/src/affiliate/utils.ts @@ -2,7 +2,7 @@ import { ForbiddenException } from '@nestjs/common' import type { SiweRequest } from './siwe-auth.guard' -export const PARTNER_CODE_REGEX = /^[a-zA-Z0-9-]{3,32}$/ +export const PARTNER_CODE_REGEX = /^[a-z0-9]{3,32}$/ export const RESERVED_PARTNER_CODES = ['ss', 'admin', 'api', 'test', 'demo'] export function assertSiweMatches(req: SiweRequest, target: string, message: string): void { From 433e8fb3c20a692dfb7a336303f65952921169c5 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:03:07 -0600 Subject: [PATCH 2/9] feat(swap-service): require partnerCode and bps on CreateAffiliateDto --- apps/swap-service/src/affiliate/types.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/swap-service/src/affiliate/types.ts b/apps/swap-service/src/affiliate/types.ts index b3197cf..d492982 100644 --- a/apps/swap-service/src/affiliate/types.ts +++ b/apps/swap-service/src/affiliate/types.ts @@ -53,15 +53,13 @@ export class CreateAffiliateDto { @IsEthereumAddress() receiveAddress?: string - @IsOptional() @Matches(PARTNER_CODE_REGEX, { message: PARTNER_CODE_MESSAGE }) - partnerCode?: string + partnerCode: string - @IsOptional() @IsInt() @Min(0) @Max(1000) - bps?: number + bps: number } export class UpdateAffiliateDto { @@ -79,11 +77,3 @@ export class UpdateAffiliateDto { @IsBoolean() isActive?: boolean } - -export class ClaimPartnerCodeDto { - @IsEthereumAddress() - walletAddress: string - - @Matches(PARTNER_CODE_REGEX, { message: PARTNER_CODE_MESSAGE }) - partnerCode: string -} From ae1a075eb3d4993d16d7f2f0eb4e3069bcc45311 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:10:25 -0600 Subject: [PATCH 3/9] test(swap-service): add affiliate.service createAffiliate tests --- .../__tests__/affiliate.service.test.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts diff --git a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts new file mode 100644 index 0000000..910f666 --- /dev/null +++ b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts @@ -0,0 +1,89 @@ +import type { Affiliate } from '@prisma/client' + +import type { PrismaService } from '../../prisma/prisma.service' +import { AffiliateService } from '../affiliate.service' + +type MockAffiliateDelegate = { + findUnique: jest.Mock + create: jest.Mock +} + +const makePrismaMock = (overrides?: Partial): PrismaService => { + const affiliate: MockAffiliateDelegate = { + findUnique: jest.fn().mockResolvedValue(null), + create: jest.fn(), + ...overrides, + } + return { affiliate } as unknown as PrismaService +} + +const baseRow = (overrides?: Partial): Affiliate => ({ + id: 'a1', + walletAddress: '0x1111111111111111111111111111111111111111', + receiveAddress: null, + partnerCode: 'goodcode', + bps: 60, + isActive: true, + createdAt: new Date('2026-05-28T00:00:00.000Z'), + updatedAt: new Date('2026-05-28T00:00:00.000Z'), + ...overrides, +}) + +describe('AffiliateService.createAffiliate', () => { + const wallet = '0x1111111111111111111111111111111111111111' + + it('creates a row when wallet and code are unused', async () => { + const create = jest.fn().mockResolvedValue(baseRow()) + const prisma = makePrismaMock({ create }) + const service = new AffiliateService(prisma) + + const result = await service.createAffiliate({ + walletAddress: wallet, + partnerCode: 'goodcode', + bps: 75, + }) + + expect(create).toHaveBeenCalledWith({ + data: { + walletAddress: wallet, + receiveAddress: undefined, + partnerCode: 'goodcode', + bps: 75, + }, + }) + expect(result.walletAddress).toBe(wallet) + }) + + it('rejects when wallet is already registered', async () => { + const prisma = makePrismaMock({ + findUnique: jest.fn().mockResolvedValueOnce(baseRow()), + }) + const service = new AffiliateService(prisma) + + await expect( + service.createAffiliate({ walletAddress: wallet, partnerCode: 'newcode', bps: 60 }), + ).rejects.toThrow(/already/) + }) + + it('rejects reserved partner codes (case-insensitive)', async () => { + const prisma = makePrismaMock() + const service = new AffiliateService(prisma) + + await expect( + service.createAffiliate({ walletAddress: wallet, partnerCode: 'ADMIN', bps: 60 }), + ).rejects.toThrow(/reserved/) + }) + + it('rejects when partner code is already taken', async () => { + const findUnique = jest + .fn() + .mockResolvedValueOnce(null) // wallet lookup + .mockResolvedValueOnce(baseRow({ walletAddress: '0xother', partnerCode: 'goodcode' })) + const prisma = makePrismaMock({ findUnique }) + const service = new AffiliateService(prisma) + + await expect( + service.createAffiliate({ walletAddress: wallet, partnerCode: 'goodcode', bps: 60 }), + ).rejects.toThrow(/taken/) + }) +}) From 6e8ffbc3adc28d1332383a9054ae01a6015e0414 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:11:27 -0600 Subject: [PATCH 4/9] feat(swap-service): enforce reserved-code check at register, drop claimPartnerCode --- .../src/affiliate/affiliate.service.ts | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index 4307187..ff896f9 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -38,12 +38,16 @@ export class AffiliateService { const existing = await this.prisma.affiliate.findUnique({ where: { walletAddress } }) if (existing) throw new Error('Affiliate already registered') - if (data.partnerCode) { - const existingCode = await this.prisma.affiliate.findUnique({ where: { partnerCode } }) - if (existingCode) throw new Error('Partner code already taken') + if (RESERVED_PARTNER_CODES.includes(partnerCode.toLowerCase())) { + throw new Error('This partner code is reserved') } - return this.prisma.affiliate.create({ data: { walletAddress, receiveAddress, partnerCode, bps: bps ?? 60 } }) + const existingCode = await this.prisma.affiliate.findUnique({ where: { partnerCode } }) + if (existingCode) throw new Error('Partner code already taken') + + return this.prisma.affiliate.create({ + data: { walletAddress, receiveAddress, partnerCode, bps }, + }) } async updateAffiliate(walletAddress: string, data: UpdateAffiliateDto): Promise { @@ -62,22 +66,7 @@ export class AffiliateService { return this.prisma.affiliate.update({ where: { walletAddress }, data: updateData }) } - async claimPartnerCode(walletAddress: string, partnerCode: string): Promise { - if (RESERVED_PARTNER_CODES.includes(partnerCode.toLowerCase())) throw new Error('This partner code is reserved') - - const existingCode = await this.prisma.affiliate.findUnique({ where: { partnerCode } }) - if (existingCode && existingCode.walletAddress.toLowerCase() !== walletAddress.toLowerCase()) { - throw new Error('Partner code already taken') - } - - return this.prisma.affiliate.upsert({ - where: { walletAddress }, - update: { partnerCode }, - create: { walletAddress, partnerCode, bps: 60 }, - }) - } - - async getAffiliateStats(address: string, options: AffiliateStatsQueryDto): Promise { +async getAffiliateStats(address: string, options: AffiliateStatsQueryDto): Promise { const { startDate, endDate } = options const items = await this.prisma.swap.findMany({ From e6c0952dc4d60516446408339fbf8db3c98dce45 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:18:29 -0600 Subject: [PATCH 5/9] style(swap-service): fix getAffiliateStats indentation Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/swap-service/src/affiliate/affiliate.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index ff896f9..f1fb8cd 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -66,7 +66,7 @@ export class AffiliateService { return this.prisma.affiliate.update({ where: { walletAddress }, data: updateData }) } -async getAffiliateStats(address: string, options: AffiliateStatsQueryDto): Promise { + async getAffiliateStats(address: string, options: AffiliateStatsQueryDto): Promise { const { startDate, endDate } = options const items = await this.prisma.swap.findMany({ From 5e64fd9b57ba33461897ebc8184f7353c8bbc6ec Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:19:58 -0600 Subject: [PATCH 6/9] feat(swap-service): remove POST /v1/affiliate/claim-code route --- .../src/affiliate/affiliate.controller.ts | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/apps/swap-service/src/affiliate/affiliate.controller.ts b/apps/swap-service/src/affiliate/affiliate.controller.ts index 1513458..dfae1ee 100644 --- a/apps/swap-service/src/affiliate/affiliate.controller.ts +++ b/apps/swap-service/src/affiliate/affiliate.controller.ts @@ -21,7 +21,6 @@ import { SiweAuthGuard, SiweRequest } from './siwe-auth.guard' import { AffiliateStatsQueryDto, AffiliateSwapsQueryDto, - ClaimPartnerCodeDto, CreateAffiliateDto, UpdateAffiliateDto, } from './types' @@ -62,7 +61,10 @@ export class AffiliateController { return toAffiliateResponse(await this.affiliateService.createAffiliate(data)) } catch (error) { if (error instanceof Error) { - if (error.message.includes('already')) throw new ConflictException(error.message) + if (error.message.includes('reserved')) throw new BadRequestException(error.message) + if (error.message.includes('already') || error.message.includes('taken')) { + throw new ConflictException(error.message) + } } throw error } @@ -75,26 +77,6 @@ export class AffiliateController { return toAffiliateResponse(await this.affiliateService.updateAffiliate(address, data)) } - - @UseGuards(SiweAuthGuard) - @Post('claim-code') - async claimPartnerCode(@Req() req: SiweRequest, @Body() data: ClaimPartnerCodeDto) { - assertSiweMatches(req, data.walletAddress, 'Authenticated address does not match walletAddress') - - try { - 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')) { - throw new ConflictException(error.message) - } - if (error.message.includes('must be')) { - throw new BadRequestException(error.message) - } - } - throw error - } - } } @Controller('v1/partner') From 53bafd0139922870e4857b947e947699cc5875d1 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:34:27 -0600 Subject: [PATCH 7/9] feat(swap-service): expand RESERVED_PARTNER_CODES Adds ShapeShift brand terms (shapeshift, shape, fox, shapeshiftdao) and internal/technical reserved names (root, system, support, dev, staging, prod, www, login, etc.) to prevent collision and impersonation. Existing entries preserved. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/swap-service/src/affiliate/utils.ts | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/swap-service/src/affiliate/utils.ts b/apps/swap-service/src/affiliate/utils.ts index eb94a59..638ccd5 100644 --- a/apps/swap-service/src/affiliate/utils.ts +++ b/apps/swap-service/src/affiliate/utils.ts @@ -3,7 +3,40 @@ import { ForbiddenException } from '@nestjs/common' import type { SiweRequest } from './siwe-auth.guard' export const PARTNER_CODE_REGEX = /^[a-z0-9]{3,32}$/ -export const RESERVED_PARTNER_CODES = ['ss', 'admin', 'api', 'test', 'demo'] +export const RESERVED_PARTNER_CODES = [ + 'admin', + 'api', + 'auth', + 'demo', + 'dev', + 'email', + 'fox', + 'graphql', + 'help', + 'internal', + 'login', + 'logout', + 'mail', + 'prod', + 'production', + 'qa', + 'register', + 'root', + 'rpc', + 'shape', + 'shapeshift', + 'shapeshiftdao', + 'signin', + 'signup', + 'ss', + 'staff', + 'staging', + 'support', + 'system', + 'test', + 'webhook', + 'www', +] export function assertSiweMatches(req: SiweRequest, target: string, message: string): void { if (req.siweAddress !== target.toLowerCase()) throw new ForbiddenException(message) From a3a2ad7d5fa912feb13861068ab91e0bd2e10645 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:40:37 -0600 Subject: [PATCH 8/9] refactor(swap-service): trim reserved partner codes; add substring helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the account-impersonation entries (login/signin/support/etc.) that don't match the actual threat model — partner codes aren't shown as a trust signal anywhere user-facing. Keeps brand short forms and dev/protocol names. Adds an isReservedPartnerCode helper with a substring match list seeded with "shapeshift" to block codes like "myshapeshift" or "shapeshift-airdrop". Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/affiliate.service.test.ts | 18 ++++++++-------- .../src/affiliate/affiliate.controller.ts | 7 +------ .../src/affiliate/affiliate.service.ts | 4 ++-- apps/swap-service/src/affiliate/utils.ts | 21 +++++++++---------- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts index 910f666..c2fee7d 100644 --- a/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts +++ b/apps/swap-service/src/affiliate/__tests__/affiliate.service.test.ts @@ -60,18 +60,18 @@ describe('AffiliateService.createAffiliate', () => { }) const service = new AffiliateService(prisma) - await expect( - service.createAffiliate({ walletAddress: wallet, partnerCode: 'newcode', bps: 60 }), - ).rejects.toThrow(/already/) + await expect(service.createAffiliate({ walletAddress: wallet, partnerCode: 'newcode', bps: 60 })).rejects.toThrow( + /already/, + ) }) it('rejects reserved partner codes (case-insensitive)', async () => { const prisma = makePrismaMock() const service = new AffiliateService(prisma) - await expect( - service.createAffiliate({ walletAddress: wallet, partnerCode: 'ADMIN', bps: 60 }), - ).rejects.toThrow(/reserved/) + await expect(service.createAffiliate({ walletAddress: wallet, partnerCode: 'ADMIN', bps: 60 })).rejects.toThrow( + /reserved/, + ) }) it('rejects when partner code is already taken', async () => { @@ -82,8 +82,8 @@ describe('AffiliateService.createAffiliate', () => { const prisma = makePrismaMock({ findUnique }) const service = new AffiliateService(prisma) - await expect( - service.createAffiliate({ walletAddress: wallet, partnerCode: 'goodcode', bps: 60 }), - ).rejects.toThrow(/taken/) + await expect(service.createAffiliate({ walletAddress: wallet, partnerCode: 'goodcode', bps: 60 })).rejects.toThrow( + /taken/, + ) }) }) diff --git a/apps/swap-service/src/affiliate/affiliate.controller.ts b/apps/swap-service/src/affiliate/affiliate.controller.ts index dfae1ee..764a007 100644 --- a/apps/swap-service/src/affiliate/affiliate.controller.ts +++ b/apps/swap-service/src/affiliate/affiliate.controller.ts @@ -18,12 +18,7 @@ import { SHAPESHIFT_BPS } from '../swaps/constants' import { AffiliateService } from './affiliate.service' import { SiweAuthGuard, SiweRequest } from './siwe-auth.guard' -import { - AffiliateStatsQueryDto, - AffiliateSwapsQueryDto, - CreateAffiliateDto, - UpdateAffiliateDto, -} from './types' +import { AffiliateStatsQueryDto, AffiliateSwapsQueryDto, CreateAffiliateDto, UpdateAffiliateDto } from './types' import { assertSiweMatches } from './utils' const toAffiliateResponse = (affiliate: Affiliate) => { diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index f1fb8cd..99c9919 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -14,7 +14,7 @@ import type { CreateAffiliateDto, UpdateAffiliateDto, } from './types' -import { RESERVED_PARTNER_CODES } from './utils' +import { isReservedPartnerCode } from './utils' @Injectable() export class AffiliateService { @@ -38,7 +38,7 @@ export class AffiliateService { const existing = await this.prisma.affiliate.findUnique({ where: { walletAddress } }) if (existing) throw new Error('Affiliate already registered') - if (RESERVED_PARTNER_CODES.includes(partnerCode.toLowerCase())) { + if (isReservedPartnerCode(partnerCode)) { throw new Error('This partner code is reserved') } diff --git a/apps/swap-service/src/affiliate/utils.ts b/apps/swap-service/src/affiliate/utils.ts index 638ccd5..26dfc73 100644 --- a/apps/swap-service/src/affiliate/utils.ts +++ b/apps/swap-service/src/affiliate/utils.ts @@ -3,7 +3,8 @@ import { ForbiddenException } from '@nestjs/common' import type { SiweRequest } from './siwe-auth.guard' export const PARTNER_CODE_REGEX = /^[a-z0-9]{3,32}$/ -export const RESERVED_PARTNER_CODES = [ + +const RESERVED_PARTNER_CODES = [ 'admin', 'api', 'auth', @@ -12,32 +13,30 @@ export const RESERVED_PARTNER_CODES = [ 'email', 'fox', 'graphql', - 'help', 'internal', - 'login', - 'logout', 'mail', 'prod', 'production', 'qa', - 'register', 'root', 'rpc', 'shape', - 'shapeshift', - 'shapeshiftdao', - 'signin', - 'signup', 'ss', - 'staff', 'staging', - 'support', 'system', 'test', 'webhook', 'www', ] +const RESERVED_PARTNER_CODE_SUBSTRINGS = ['shapeshift'] + +export function isReservedPartnerCode(partnerCode: string): boolean { + const code = partnerCode.toLowerCase() + if (RESERVED_PARTNER_CODES.includes(code)) return true + return RESERVED_PARTNER_CODE_SUBSTRINGS.some((substring) => code.includes(substring)) +} + export function assertSiweMatches(req: SiweRequest, target: string, message: string): void { if (req.siweAddress !== target.toLowerCase()) throw new ForbiddenException(message) } From 3b751b07923d7a3301a17e3c2dcc40f60b4d1a8b Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Thu, 28 May 2026 16:43:11 -0600 Subject: [PATCH 9/9] refactor(swap-service): further trim RESERVED_PARTNER_CODES Narrows the exact-match list to envs (dev/develop/staging? prod/production/release/qa? trimmed by user), brand short forms, and a few system terms. Substring list keeps "shapeshift". Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/affiliate/affiliate.service.ts | 4 +--- apps/swap-service/src/affiliate/utils.ts | 14 ++------------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/apps/swap-service/src/affiliate/affiliate.service.ts b/apps/swap-service/src/affiliate/affiliate.service.ts index 99c9919..177347b 100644 --- a/apps/swap-service/src/affiliate/affiliate.service.ts +++ b/apps/swap-service/src/affiliate/affiliate.service.ts @@ -38,9 +38,7 @@ export class AffiliateService { const existing = await this.prisma.affiliate.findUnique({ where: { walletAddress } }) if (existing) throw new Error('Affiliate already registered') - if (isReservedPartnerCode(partnerCode)) { - throw new Error('This partner code is reserved') - } + if (isReservedPartnerCode(partnerCode)) throw new Error('This partner code is reserved') const existingCode = await this.prisma.affiliate.findUnique({ where: { partnerCode } }) if (existingCode) throw new Error('Partner code already taken') diff --git a/apps/swap-service/src/affiliate/utils.ts b/apps/swap-service/src/affiliate/utils.ts index 26dfc73..de9e6df 100644 --- a/apps/swap-service/src/affiliate/utils.ts +++ b/apps/swap-service/src/affiliate/utils.ts @@ -10,23 +10,13 @@ const RESERVED_PARTNER_CODES = [ 'auth', 'demo', 'dev', - 'email', - 'fox', - 'graphql', - 'internal', - 'mail', + 'develop', 'prod', 'production', - 'qa', - 'root', - 'rpc', - 'shape', + 'release', 'ss', - 'staging', 'system', 'test', - 'webhook', - 'www', ] const RESERVED_PARTNER_CODE_SUBSTRINGS = ['shapeshift']