From 368742b41faa354ee1aeb0b4873df9a5cdbbaf96 Mon Sep 17 00:00:00 2001 From: Jerry Fan Date: Wed, 11 Sep 2024 11:21:01 -0400 Subject: [PATCH 1/6] modify affiliate api stubs --- .../api/v4/affiliates-controller.test.ts | 4 +-- .../comlink/public/api-documentation.md | 28 +++++++++++-------- indexer/services/comlink/public/swagger.json | 19 ++++++++----- .../api/v4/affiliates-controller.ts | 15 +++++----- indexer/services/comlink/src/types.ts | 3 +- 5 files changed, 40 insertions(+), 29 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 09c0a3ba70..ff203417a5 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 @@ -3,12 +3,12 @@ import request from 'supertest'; import { sendRequest } from '../../../helpers/helpers'; describe('affiliates-controller#V4', () => { - describe('GET /referral_code', () => { + describe('GET /metadata', () => { it('should return referral code for a valid address string', async () => { const address = 'some_address'; const response: request.Response = await sendRequest({ type: RequestMethod.GET, - path: `/v4/affiliates/referral_code?address=${address}`, + path: `/v4/affiliates/metadata?address=${address}`, }); expect(response.status).toBe(200); diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md index f5cf63ba61..4c56a11551 100644 --- a/indexer/services/comlink/public/api-documentation.md +++ b/indexer/services/comlink/public/api-documentation.md @@ -368,9 +368,9 @@ fetch(`${baseURL}/addresses/{address}/parentSubaccountNumber/{parentSubaccountNu This operation does not require authentication -## GetReferralCode +## GetMetadata - + > Code samples @@ -384,7 +384,7 @@ headers = { # baseURL = 'https://indexer.dydx.trade/v4' baseURL = 'https://dydx-testnet.imperator.co/v4' -r = requests.get(f'{baseURL}/affiliates/referral_code', params={ +r = requests.get(f'{baseURL}/affiliates/metadata', params={ 'address': 'string' }, headers = headers) @@ -402,7 +402,7 @@ const headers = { // const baseURL = 'https://indexer.dydx.trade/v4'; const baseURL = 'https://dydx-testnet.imperator.co/v4'; -fetch(`${baseURL}/affiliates/referral_code?address=string`, +fetch(`${baseURL}/affiliates/metadata?address=string`, { method: 'GET', @@ -416,7 +416,7 @@ fetch(`${baseURL}/affiliates/referral_code?address=string`, ``` -`GET /affiliates/referral_code` +`GET /affiliates/metadata` ### Parameters @@ -509,12 +509,13 @@ fetch(`${baseURL}/affiliates/snapshot`, "affiliateList": [ { "affiliateAddress": "string", - "affiliateEarnings": 0.1, "affiliateReferralCode": "string", + "affiliateEarnings": 0.1, "affiliateReferredTrades": 0.1, "affiliateTotalReferredFees": 0.1, "affiliateReferredUsers": 0.1, - "affiliateReferredNetProtocolEarnings": 0.1 + "affiliateReferredNetProtocolEarnings": 0.1, + "affiliateReferredTotalVolume": 0.1 } ], "total": 0.1, @@ -4069,12 +4070,13 @@ This operation does not require authentication ```json { "affiliateAddress": "string", - "affiliateEarnings": 0.1, "affiliateReferralCode": "string", + "affiliateEarnings": 0.1, "affiliateReferredTrades": 0.1, "affiliateTotalReferredFees": 0.1, "affiliateReferredUsers": 0.1, - "affiliateReferredNetProtocolEarnings": 0.1 + "affiliateReferredNetProtocolEarnings": 0.1, + "affiliateReferredTotalVolume": 0.1 } ``` @@ -4084,12 +4086,13 @@ This operation does not require authentication |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |affiliateAddress|string|true|none|none| -|affiliateEarnings|number(double)|true|none|none| |affiliateReferralCode|string|true|none|none| +|affiliateEarnings|number(double)|true|none|none| |affiliateReferredTrades|number(double)|true|none|none| |affiliateTotalReferredFees|number(double)|true|none|none| |affiliateReferredUsers|number(double)|true|none|none| |affiliateReferredNetProtocolEarnings|number(double)|true|none|none| +|affiliateReferredTotalVolume|number(double)|true|none|none| ## AffiliateSnapshotResponse @@ -4103,12 +4106,13 @@ This operation does not require authentication "affiliateList": [ { "affiliateAddress": "string", - "affiliateEarnings": 0.1, "affiliateReferralCode": "string", + "affiliateEarnings": 0.1, "affiliateReferredTrades": 0.1, "affiliateTotalReferredFees": 0.1, "affiliateReferredUsers": 0.1, - "affiliateReferredNetProtocolEarnings": 0.1 + "affiliateReferredNetProtocolEarnings": 0.1, + "affiliateReferredTotalVolume": 0.1 } ], "total": 0.1, diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json index 1054683fcd..1113841e85 100644 --- a/indexer/services/comlink/public/swagger.json +++ b/indexer/services/comlink/public/swagger.json @@ -271,13 +271,13 @@ "affiliateAddress": { "type": "string" }, + "affiliateReferralCode": { + "type": "string" + }, "affiliateEarnings": { "type": "number", "format": "double" }, - "affiliateReferralCode": { - "type": "string" - }, "affiliateReferredTrades": { "type": "number", "format": "double" @@ -293,16 +293,21 @@ "affiliateReferredNetProtocolEarnings": { "type": "number", "format": "double" + }, + "affiliateReferredTotalVolume": { + "type": "number", + "format": "double" } }, "required": [ "affiliateAddress", - "affiliateEarnings", "affiliateReferralCode", + "affiliateEarnings", "affiliateReferredTrades", "affiliateTotalReferredFees", "affiliateReferredUsers", - "affiliateReferredNetProtocolEarnings" + "affiliateReferredNetProtocolEarnings", + "affiliateReferredTotalVolume" ], "type": "object", "additionalProperties": false @@ -1668,9 +1673,9 @@ ] } }, - "/affiliates/referral_code": { + "/affiliates/metadata": { "get": { - "operationId": "GetReferralCode", + "operationId": "GetMetadata", "responses": { "200": { "description": "Ok", 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 f4ac198ba1..2d100df478 100644 --- a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts @@ -29,8 +29,8 @@ const controllerName: string = 'affiliates-controller'; // TODO(OTE-731): replace api stubs with real logic @Route('affiliates') class AffiliatesController extends Controller { - @Get('/referral_code') - async getReferralCode( + @Get('/metadata') + async getMetadata( @Query() address: string, // eslint-disable-line @typescript-eslint/no-unused-vars ): Promise { // simulate a delay @@ -67,12 +67,13 @@ class AffiliatesController extends Controller { const snapshot: AffiliateSnapshotResponseObject = { affiliateAddress: 'some_address', - affiliateEarnings: 100, affiliateReferralCode: 'TempCode123', + affiliateEarnings: 100, affiliateReferredTrades: 1000, affiliateTotalReferredFees: 100, affiliateReferredUsers: 10, affiliateReferredNetProtocolEarnings: 1000, + affiliateReferredTotalVolume: 1000000, }; const affiliateSnapshots: AffiliateSnapshotResponseObject[] = []; @@ -102,7 +103,7 @@ class AffiliatesController extends Controller { } router.get( - '/referral_code', + '/metadata', rateLimiterMiddleware(getReqRateLimiter), ...checkSchema({ address: { @@ -121,11 +122,11 @@ router.get( try { const controller: AffiliatesController = new AffiliatesController(); - const response: AffiliateReferralCodeResponse = await controller.getReferralCode(address); + const response: AffiliateReferralCodeResponse = await controller.getMetadata(address); return res.send(response); } catch (error) { return handleControllerError( - 'AffiliatesController GET /referral_code', + 'AffiliatesController GET /metadata', 'Affiliates referral code error', error, req, @@ -133,7 +134,7 @@ router.get( ); } finally { stats.timing( - `${config.SERVICE_NAME}.${controllerName}.get_referral_code.timing`, + `${config.SERVICE_NAME}.${controllerName}.get_metadata.timing`, Date.now() - start, ); } diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index 45622e56d5..894caba79a 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -703,12 +703,13 @@ export interface AffiliateSnapshotResponse { export interface AffiliateSnapshotResponseObject { affiliateAddress: string, - affiliateEarnings: number, affiliateReferralCode: string, + affiliateEarnings: number, affiliateReferredTrades: number, affiliateTotalReferredFees: number, affiliateReferredUsers: number, affiliateReferredNetProtocolEarnings: number, + affiliateReferredTotalVolume: number, } export interface AffiliateTotalVolumeResponse { From bc16518230b687c8eecb1ccb2e458a60a5899f17 Mon Sep 17 00:00:00 2001 From: Jerry Fan Date: Wed, 11 Sep 2024 13:27:16 -0400 Subject: [PATCH 2/6] implement comlink GET affiliates/metadata --- indexer/services/comlink/src/config.ts | 3 ++ .../api/v4/affiliates-controller.ts | 51 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) 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 2d100df478..768cffccd6 100644 --- a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts @@ -4,7 +4,13 @@ import { checkSchema, matchedData } from 'express-validator'; import { Controller, Get, Query, Route, } from 'tsoa'; - +import { + WalletTable, + AffiliateReferredUsersTable, + SubaccountTable, + SubaccountUsernamesTable, +} from '@dydxprotocol-indexer/postgres'; +import { NotFoundError, UnexpectedServerError } from '../../../lib/errors'; import { getReqRateLimiter } from '../../../caches/rate-limiters'; import config from '../../../config'; import { handleControllerError } from '../../../lib/helpers'; @@ -31,12 +37,47 @@ 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)); + // Check that the address exists + const walletRow = await WalletTable.findById(address); + if (!walletRow) { + throw new NotFoundError(`Wallet with address ${address} not found`); + } + const isVolumeEligible = Number(walletRow.totalVolume) >= config.VOLUME_ELIGIBILITY_THRESHOLD; + + // Check if the address is an affiliate (has referred users) + const referredUserRows = await AffiliateReferredUsersTable.findByAffiliateAddress(address); + const isAffiliate = referredUserRows != undefined ? referredUserRows.length > 0 : false; + + // Get referral code (subaccount 0 username) + const subaccountRows = await SubaccountTable.findAll( + { + address: address, + subaccountNumber: 0, + }, + [], + ) + // No need to check subaccountRows.length > 1 because subaccountNumber is unique for an address + if (subaccountRows.length === 0) { + throw new UnexpectedServerError(`Subaccount 0 not found for address ${address}`); + } + const subaccountId = subaccountRows[0].id; + const usernameRows = await SubaccountUsernamesTable.findAll( + { + subaccountId: [subaccountId], + }, + [], + ) + if (usernameRows.length === 0) { + throw new UnexpectedServerError(`Username not found for subaccount ${subaccountId}`); + } else if (usernameRows.length > 1) { + throw new UnexpectedServerError(`Found multiple usernames for subaccount ${subaccountId}`); + } + const referralCode = usernameRows[0].username; + return { - referralCode: 'TempCode123', + referralCode: referralCode, }; } From ac62c32c015dbe47726ef37aa3ce855e75f8fc6e Mon Sep 17 00:00:00 2001 From: Jerry Fan Date: Thu, 12 Sep 2024 12:42:26 -0400 Subject: [PATCH 3/6] tests --- .../api/v4/affiliates-controller.test.ts | 127 +++++++++++++++++- indexer/services/comlink/package.json | 2 +- .../api/v4/affiliates-controller.ts | 19 +-- 3 files changed, 126 insertions(+), 22 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..7cf7847a79 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,22 +1,139 @@ +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' + const response: request.Response = 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 + WalletTable.create(testConstants.defaultWallet2); + const response: request.Response = await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/metadata?address=${testConstants.defaultWallet2.address}`, + expectedStatus: 500, // helper performs expect on status + }); }); }); diff --git a/indexer/services/comlink/package.json b/indexer/services/comlink/package.json index ba12e7064b..d86c94d2c4 100644 --- a/indexer/services/comlink/package.json +++ b/indexer/services/comlink/package.json @@ -13,7 +13,7 @@ "coverage": "pnpm test -- --coverage", "lint": "eslint --ext .ts,.js .", "lint:fix": "eslint --ext .ts,.js . --fix", - "test": "NODE_ENV=test jest --runInBand --forceExit", + "test-comlink": "NODE_ENV=test jest --runInBand --forceExit", "swagger": "ts-node -r dotenv-flow/config src/scripts/generate-swagger.ts", "gen-markdown": "widdershins public/swagger.json -o public/api-documentation.md --omitHeader --language_tabs 'python:Python' 'javascript:Javascript'" }, 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 34be54dbad..78d51a9a32 100644 --- a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts @@ -37,9 +37,8 @@ const controllerName: string = 'affiliates-controller'; class AffiliatesController extends Controller { @Get('/metadata') async getMetadata( -<<<<<<< HEAD @Query() address: string, - ): Promise { + ): Promise { // Check that the address exists const walletRow = await WalletTable.findById(address); if (!walletRow) { @@ -79,16 +78,8 @@ class AffiliatesController extends Controller { return { referralCode: referralCode, -======= - @Query() address: string, // eslint-disable-line @typescript-eslint/no-unused-vars - ): Promise { - // simulate a delay - await new Promise((resolve) => setTimeout(resolve, 100)); - return { - referralCode: 'TempCode123', - isVolumeEligible: true, - isAffiliate: false, ->>>>>>> main + isVolumeEligible: isVolumeEligible, + isAffiliate: isAffiliate, }; } @@ -174,11 +165,7 @@ router.get( try { const controller: AffiliatesController = new AffiliatesController(); -<<<<<<< HEAD - const response: AffiliateReferralCodeResponse = await controller.getMetadata(address); -======= const response: AffiliateMetadataResponse = await controller.getMetadata(address); ->>>>>>> main return res.send(response); } catch (error) { return handleControllerError( From 0784cfa3b7de2e20133430e7f241fd6385a8bc96 Mon Sep 17 00:00:00 2001 From: Jerry Fan Date: Thu, 12 Sep 2024 12:47:33 -0400 Subject: [PATCH 4/6] revert package.json --- indexer/services/comlink/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/services/comlink/package.json b/indexer/services/comlink/package.json index 5e8085911c..29ea1def60 100644 --- a/indexer/services/comlink/package.json +++ b/indexer/services/comlink/package.json @@ -13,7 +13,7 @@ "coverage": "pnpm test -- --coverage", "lint": "eslint --ext .ts,.js .", "lint:fix": "eslint --ext .ts,.js . --fix", - "test-comlink": "NODE_ENV=test jest --runInBand --forceExit", + "test": "NODE_ENV=test jest --runInBand --forceExit", "swagger": "ts-node -r dotenv-flow/config src/scripts/generate-swagger.ts", "gen-markdown": "widdershins public/swagger.json -o public/api-documentation.md --omitHeader --language_tabs 'python:Python' 'javascript:Javascript'" }, From 21481ecbb9b98843c8c437ed736168883ed224c1 Mon Sep 17 00:00:00 2001 From: Jerry Fan Date: Thu, 12 Sep 2024 12:58:52 -0400 Subject: [PATCH 5/6] lint fix --- .../api/v4/affiliates-controller.test.ts | 28 ++++++++-------- .../api/v4/affiliates-controller.ts | 33 ++++++++++--------- 2 files changed, 31 insertions(+), 30 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 7cf7847a79..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 @@ -25,7 +25,7 @@ describe('affiliates-controller#V4', () => { await testMocks.seedData(); await SubaccountUsernamesTable.create(testConstants.defaultSubaccountUsername); }); - + afterEach(async () => { await dbHelpers.clearData(); }); @@ -46,20 +46,20 @@ describe('affiliates-controller#V4', () => { }); it('should fail if address does not exist', async () => { - const nonExistentAddress = 'adgsakhasgt' - const response: request.Response = await sendRequest({ + 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 () => { + it('should classify not volume eligible', async () => { await WalletTable.update( - { + { address: testConstants.defaultWallet.address, - totalVolume: "0", - totalTradingRewards: "0", + totalVolume: '0', + totalTradingRewards: '0', }, ); const response: request.Response = await sendRequest({ @@ -74,12 +74,12 @@ describe('affiliates-controller#V4', () => { }); }); - it('should classify volume eligible', async () => { + it('should classify volume eligible', async () => { await WalletTable.update( - { + { address: testConstants.defaultWallet.address, - totalVolume: "100000", - totalTradingRewards: "0", + totalVolume: '100000', + totalTradingRewards: '0', }, ); const response: request.Response = await sendRequest({ @@ -92,7 +92,7 @@ describe('affiliates-controller#V4', () => { isVolumeEligible: true, isAffiliate: false, }); - }); + }); it('should classify is not affiliate', async () => { // AffiliateReferredUsersTable is empty @@ -128,8 +128,8 @@ describe('affiliates-controller#V4', () => { it('should fail if subaccount username not found', async () => { // create defaultWallet2 without subaccount username - WalletTable.create(testConstants.defaultWallet2); - const response: request.Response = await sendRequest({ + 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 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 78d51a9a32..15ca181906 100644 --- a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts @@ -1,18 +1,19 @@ import { stats } from '@dydxprotocol-indexer/base'; -import express from 'express'; -import { checkSchema, matchedData } from 'express-validator'; -import { - Controller, Get, Query, Route, -} from 'tsoa'; import { WalletTable, AffiliateReferredUsersTable, SubaccountTable, SubaccountUsernamesTable, } from '@dydxprotocol-indexer/postgres'; -import { NotFoundError, UnexpectedServerError } from '../../../lib/errors'; +import express from 'express'; +import { checkSchema, matchedData } from 'express-validator'; +import { + Controller, Get, Query, Route, +} from 'tsoa'; + 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'; @@ -48,38 +49,38 @@ class AffiliatesController extends Controller { // Check if the address is an affiliate (has referred users) const referredUserRows = await AffiliateReferredUsersTable.findByAffiliateAddress(address); - const isAffiliate = referredUserRows != undefined ? referredUserRows.length > 0 : false; + const isAffiliate = referredUserRows !== undefined ? referredUserRows.length > 0 : false; // Get referral code (subaccount 0 username) const subaccountRows = await SubaccountTable.findAll( { - address: address, + address, subaccountNumber: 0, }, [], - ) - // No need to check subaccountRows.length > 1 because subaccountNumber is unique for an address + ); + // No need to check subaccountRows.length > 1 as subaccountNumber is unique for an address if (subaccountRows.length === 0) { throw new UnexpectedServerError(`Subaccount 0 not found for address ${address}`); } const subaccountId = subaccountRows[0].id; + const usernameRows = await SubaccountUsernamesTable.findAll( { subaccountId: [subaccountId], }, [], - ) + ); + // No need to check usernameRows.length > 1 as subAccountId is unique (foreign key constraint) if (usernameRows.length === 0) { throw new UnexpectedServerError(`Username not found for subaccount ${subaccountId}`); - } else if (usernameRows.length > 1) { - throw new UnexpectedServerError(`Found multiple usernames for subaccount ${subaccountId}`); } const referralCode = usernameRows[0].username; return { - referralCode: referralCode, - isVolumeEligible: isVolumeEligible, - isAffiliate: isAffiliate, + referralCode, + isVolumeEligible, + isAffiliate, }; } From 3fa0437059f24c2b29a8d8df30c919497afdd88c Mon Sep 17 00:00:00 2001 From: Jerry Fan Date: Thu, 12 Sep 2024 16:01:09 -0400 Subject: [PATCH 6/6] replace affiliates address and total_volume stubs with real implementation --- .../api/v4/affiliates-controller.test.ts | 63 +++++++++++++++---- .../api/v4/affiliates-controller.ts | 34 +++++++--- indexer/services/comlink/src/types.ts | 2 +- 3 files changed, 78 insertions(+), 21 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 465865020d..62ee29513e 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 @@ -138,16 +138,34 @@ describe('affiliates-controller#V4', () => { }); describe('GET /address', () => { - it('should return address for a valid referral code string', async () => { - const referralCode = 'TempCode123'; + beforeEach(async () => { + await testMocks.seedData(); + await SubaccountUsernamesTable.create(testConstants.defaultSubaccountUsername); + }); + + afterEach(async () => { + await dbHelpers.clearData(); + }); + + it('should return address for a valid referral code', async () => { + const referralCode = testConstants.defaultSubaccountUsername.username; const response: request.Response = await sendRequest({ type: RequestMethod.GET, path: `/v4/affiliates/address?referralCode=${referralCode}`, + expectedStatus: 200, // helper performs expect on status }); - expect(response.status).toBe(200); expect(response.body).toEqual({ - address: 'some_address', + address: testConstants.defaultWallet.address, + }); + }); + + it('should fail when referral code not found', async () => { + const nonExistentReferralCode = 'BadCode123'; + await sendRequest({ + type: RequestMethod.GET, + path: `/v4/affiliates/address?referralCode=${nonExistentReferralCode}`, + expectedStatus: 404, // helper performs expect on status }); }); }); @@ -157,11 +175,11 @@ describe('affiliates-controller#V4', () => { const req: AffiliateSnapshotRequest = { limit: 10, offset: 10, - sortByReferredFees: true, + sortByAffiliateEarning: true, }; const response: request.Response = await sendRequest({ type: RequestMethod.GET, - path: `/v4/affiliates/snapshot?limit=${req.limit}&offset=${req.offset}&sortByReferredFees=${req.sortByReferredFees}`, + path: `/v4/affiliates/snapshot?limit=${req.limit}&offset=${req.offset}&sortByReferredFees=${req.sortByAffiliateEarning}`, }); expect(response.status).toBe(200); @@ -184,16 +202,39 @@ describe('affiliates-controller#V4', () => { }); describe('GET /total_volume', () => { - it('should return total_volume for a valid address', async () => { - const address = 'some_address'; + beforeEach(async () => { + await testMocks.seedData(); + await WalletTable.update( + { + address: testConstants.defaultWallet.address, + totalVolume: '100000', + totalTradingRewards: '0', + }, + ); + }); + + afterEach(async () => { + await dbHelpers.clearData(); + }); + + it('should return total volume for a valid address', async () => { const response: request.Response = await sendRequest({ type: RequestMethod.GET, - path: `/v4/affiliates/total_volume?address=${address}`, + path: `/v4/affiliates/total_volume?address=${testConstants.defaultWallet.address}`, + expectedStatus: 200, // helper performs expect on status }); - expect(response.status).toBe(200); expect(response.body).toEqual({ - totalVolume: 111.1, + totalVolume: 100000, + }); + }); + + 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 }); }); }); 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 15ca181906..03890bcde5 100644 --- a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts @@ -86,12 +86,24 @@ class AffiliatesController extends Controller { @Get('/address') async getAddress( - @Query() referralCode: string, // eslint-disable-line @typescript-eslint/no-unused-vars + @Query() referralCode: string, ): Promise { - // simulate a delay - await new Promise((resolve) => setTimeout(resolve, 100)); + const usernameRow = await SubaccountUsernamesTable.findByUsername(referralCode); + if (!usernameRow) { + throw new NotFoundError(`Referral code ${referralCode} does not exist`); + } + const subAccountId = usernameRow.subaccountId; + + const subaccountRow = await SubaccountTable.findById(subAccountId); + // subaccountRow should never be undefined because of foreign key constraint between subaccounts + // and subaccount_usernames tables + if (!subaccountRow) { + throw new UnexpectedServerError(`Subaccount ${subAccountId} not found`); + } + const address = subaccountRow.address; + return { - address: 'some_address', + address, }; } @@ -136,12 +148,16 @@ class AffiliatesController extends Controller { @Get('/total_volume') public async getTotalVolume( - @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)); + // Check that the address exists + const walletRow = await WalletTable.findById(address); + if (!walletRow) { + throw new NotFoundError(`Wallet with address ${address} not found`); + } + return { - totalVolume: 111.1, + totalVolume: Number(walletRow.totalVolume), }; } } @@ -257,7 +273,7 @@ router.get( const { offset, limit, - sortByReferredFees, + sortByAffiliateEarning: sortByReferredFees, }: AffiliateSnapshotRequest = matchedData(req) as AffiliateSnapshotRequest; try { diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts index 2867e0827b..04542f0e5e 100644 --- a/indexer/services/comlink/src/types.ts +++ b/indexer/services/comlink/src/types.ts @@ -686,7 +686,7 @@ export interface AffiliateAddressRequest{ export interface AffiliateSnapshotRequest{ limit?: number, offset?: number, - sortByReferredFees?: boolean, + sortByAffiliateEarning?: boolean, } export interface AffiliateTotalVolumeRequest{