From 9c3d96d1dbba7e5b395d1f743822514645de2864 Mon Sep 17 00:00:00 2001 From: jerryfan01234 <44346807+jerryfan01234@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:28:44 -0400 Subject: [PATCH] [OTE-760] implement comlink affiliate metadata endpoint (#2243) --- .../api/v4/affiliates-controller.test.ts | 127 +++++++++++++++++- indexer/services/comlink/src/config.ts | 3 + .../api/v4/affiliates-controller.ts | 61 ++++++++- 3 files changed, 180 insertions(+), 11 deletions(-) diff --git a/indexer/services/comlink/__tests__/controllers/api/v4/affiliates-controller.test.ts b/indexer/services/comlink/__tests__/controllers/api/v4/affiliates-controller.test.ts index 1f86ebf54e..465865020d 100644 --- a/indexer/services/comlink/__tests__/controllers/api/v4/affiliates-controller.test.ts +++ b/indexer/services/comlink/__tests__/controllers/api/v4/affiliates-controller.test.ts @@ -1,23 +1,140 @@ +import { + dbHelpers, + testConstants, + testMocks, + SubaccountUsernamesTable, + WalletTable, + AffiliateReferredUsersTable, +} from '@dydxprotocol-indexer/postgres'; import { AffiliateSnapshotRequest, RequestMethod } from '../../../../src/types'; import request from 'supertest'; import { sendRequest } from '../../../helpers/helpers'; +import { defaultWallet, defaultWallet2 } from '@dydxprotocol-indexer/postgres/build/__tests__/helpers/constants'; describe('affiliates-controller#V4', () => { + beforeAll(async () => { + await dbHelpers.migrate(); + }); + + afterAll(async () => { + await dbHelpers.teardown(); + }); + describe('GET /metadata', () => { - it('should return referral code for a valid address string', async () => { - const address = 'some_address'; + beforeEach(async () => { + await testMocks.seedData(); + await SubaccountUsernamesTable.create(testConstants.defaultSubaccountUsername); + }); + + afterEach(async () => { + await dbHelpers.clearData(); + }); + + it('should return referral code for address with username', async () => { const response: request.Response = await sendRequest({ type: RequestMethod.GET, - path: `/v4/affiliates/metadata?address=${address}`, + path: `/v4/affiliates/metadata?address=${testConstants.defaultWallet.address}`, + expectedStatus: 200, // helper performs expect on status }); - expect(response.status).toBe(200); expect(response.body).toEqual({ - referralCode: 'TempCode123', + // username is the referral code + referralCode: testConstants.defaultSubaccountUsername.username, + isVolumeEligible: false, + isAffiliate: false, + }); + }); + + it('should fail if address does not exist', async () => { + const nonExistentAddress = 'adgsakhasgt'; + await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/metadata?address=${nonExistentAddress}`, + expectedStatus: 404, // helper performs expect on status + }); + }); + + it('should classify not volume eligible', async () => { + await WalletTable.update( + { + address: testConstants.defaultWallet.address, + totalVolume: '0', + totalTradingRewards: '0', + }, + ); + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/metadata?address=${testConstants.defaultWallet.address}`, + expectedStatus: 200, // helper performs expect on status + }); + expect(response.body).toEqual({ + referralCode: testConstants.defaultSubaccountUsername.username, + isVolumeEligible: false, + isAffiliate: false, + }); + }); + + it('should classify volume eligible', async () => { + await WalletTable.update( + { + address: testConstants.defaultWallet.address, + totalVolume: '100000', + totalTradingRewards: '0', + }, + ); + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/metadata?address=${testConstants.defaultWallet.address}`, + expectedStatus: 200, // helper performs expect on status + }); + expect(response.body).toEqual({ + referralCode: testConstants.defaultSubaccountUsername.username, isVolumeEligible: true, isAffiliate: false, }); }); + + it('should classify is not affiliate', async () => { + // AffiliateReferredUsersTable is empty + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/metadata?address=${testConstants.defaultWallet.address}`, + expectedStatus: 200, // helper performs expect on status + }); + expect(response.body).toEqual({ + referralCode: testConstants.defaultSubaccountUsername.username, + isVolumeEligible: false, + isAffiliate: false, + }); + }); + + it('should classify is affiliate', async () => { + await AffiliateReferredUsersTable.create({ + affiliateAddress: defaultWallet.address, + refereeAddress: defaultWallet2.address, + referredAtBlock: '1', + }); + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/metadata?address=${testConstants.defaultWallet.address}`, + expectedStatus: 200, // helper performs expect on status + }); + expect(response.body).toEqual({ + referralCode: testConstants.defaultSubaccountUsername.username, + isVolumeEligible: false, + isAffiliate: true, + }); + }); + + it('should fail if subaccount username not found', async () => { + // create defaultWallet2 without subaccount username + await WalletTable.create(testConstants.defaultWallet2); + await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/metadata?address=${testConstants.defaultWallet2.address}`, + expectedStatus: 500, // helper performs expect on status + }); + }); }); describe('GET /address', () => { diff --git a/indexer/services/comlink/src/config.ts b/indexer/services/comlink/src/config.ts index eb3713bc14..e2c85ade2f 100644 --- a/indexer/services/comlink/src/config.ts +++ b/indexer/services/comlink/src/config.ts @@ -60,6 +60,9 @@ export const configSchema = { // vaults table is added. EXPERIMENT_VAULTS: parseString({ default: '' }), EXPERIMENT_VAULT_MARKETS: parseString({ default: '' }), + + // Affiliates config + VOLUME_ELIGIBILITY_THRESHOLD: parseInteger({ default: 10_000 }), }; //////////////////////////////////////////////////////////////////////////////// diff --git a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts index 9156fb339e..278b988422 100644 --- a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts @@ -1,4 +1,10 @@ import { stats } from '@dydxprotocol-indexer/base'; +import { + WalletTable, + AffiliateReferredUsersTable, + SubaccountTable, + SubaccountUsernamesTable, +} from '@dydxprotocol-indexer/postgres'; import express from 'express'; import { checkSchema, matchedData } from 'express-validator'; import { @@ -7,6 +13,7 @@ import { import { getReqRateLimiter } from '../../../caches/rate-limiters'; import config from '../../../config'; +import { NotFoundError, UnexpectedServerError } from '../../../lib/errors'; import { handleControllerError } from '../../../lib/helpers'; import { rateLimiterMiddleware } from '../../../lib/rate-limit'; import { handleValidationErrors } from '../../../request-helpers/error-handler'; @@ -31,14 +38,56 @@ const controllerName: string = 'affiliates-controller'; class AffiliatesController extends Controller { @Get('/metadata') async getMetadata( - @Query() address: string, // eslint-disable-line @typescript-eslint/no-unused-vars + @Query() address: string, ): Promise { - // simulate a delay - await new Promise((resolve) => setTimeout(resolve, 100)); + const [walletRow, referredUserRows, subaccountRows] = await Promise.all([ + WalletTable.findById(address), + AffiliateReferredUsersTable.findByAffiliateAddress(address), + SubaccountTable.findAll( + { + address, + subaccountNumber: 0, + }, + [], + ), + ]); + + // Check that the address exists + if (!walletRow) { + throw new NotFoundError(`Wallet with address ${address} not found`); + } + + // Check if the address is an affiliate (has referred users) + const isVolumeEligible = Number(walletRow.totalVolume) >= config.VOLUME_ELIGIBILITY_THRESHOLD; + const isAffiliate = referredUserRows !== undefined ? referredUserRows.length > 0 : false; + + // No need to check subaccountRows.length > 1 as subaccountNumber is unique for an address + if (subaccountRows.length === 0) { + // error logging will be performed by handleInternalServerError + throw new UnexpectedServerError(`Subaccount 0 not found for address ${address}`); + } + const subaccountId = subaccountRows[0].id; + + // Get subaccount0 username, which is the referral code + const usernameRows = await SubaccountUsernamesTable.findAll( + { + subaccountId: [subaccountId], + }, + [], + ); + // No need to check usernameRows.length > 1 as subAccountId is unique (foreign key constraint) + // This error can happen if a user calls this endpoint before subaccount-username-generator + // has generated the username + if (usernameRows.length === 0) { + stats.increment(`${config.SERVICE_NAME}.${controllerName}.get_metadata.subaccount_username_not_found`); + throw new UnexpectedServerError(`Username not found for subaccount ${subaccountId}`); + } + const referralCode = usernameRows[0].username; + return { - referralCode: 'TempCode123', - isVolumeEligible: true, - isAffiliate: false, + referralCode, + isVolumeEligible, + isAffiliate, }; }