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
new file mode 100644
index 00000000000..eedcca2ab09
--- /dev/null
+++ b/indexer/services/comlink/__tests__/controllers/api/v4/affiliates-controller.test.ts
@@ -0,0 +1,23 @@
+import { RequestMethod } from '../../../../src/types';
+import request from 'supertest';
+import { sendRequest } from '../../../helpers/helpers';
+
+describe('test-controller#V4', () => {
+
+ describe('GET /referral_code', () => {
+
+ 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}`,
+ });
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({
+ referralCode: `${address}-referral_code`,
+ });
+
+ });
+ });
+});
diff --git a/indexer/services/comlink/public/api-documentation.md b/indexer/services/comlink/public/api-documentation.md
index b0152c11295..5cb87ac3926 100644
--- a/indexer/services/comlink/public/api-documentation.md
+++ b/indexer/services/comlink/public/api-documentation.md
@@ -431,6 +431,82 @@ fetch(`${baseURL}/addresses/{address}/parentSubaccountNumber/{parentSubaccountNu
This operation does not require authentication
+## GetReferralCode
+
+
+
+> Code samples
+
+```python
+import requests
+headers = {
+ 'Accept': 'application/json'
+}
+
+# For the deployment by DYDX token holders, use
+# baseURL = 'https://indexer.dydx.trade/v4'
+baseURL = 'https://dydx-testnet.imperator.co/v4'
+
+r = requests.get(f'{baseURL}/affiliate/referral_code', params={
+ 'address': 'string'
+}, headers = headers)
+
+print(r.json())
+
+```
+
+```javascript
+
+const headers = {
+ 'Accept':'application/json'
+};
+
+// For the deployment by DYDX token holders, use
+// const baseURL = 'https://indexer.dydx.trade/v4';
+const baseURL = 'https://dydx-testnet.imperator.co/v4';
+
+fetch(`${baseURL}/affiliate/referral_code?address=string`,
+{
+ method: 'GET',
+
+ headers: headers
+})
+.then(function(res) {
+ return res.json();
+}).then(function(body) {
+ console.log(body);
+});
+
+```
+
+`GET /affiliate/referral_code`
+
+### Parameters
+
+|Name|In|Type|Required|Description|
+|---|---|---|---|---|
+|address|query|string|true|none|
+
+> Example responses
+
+> 200 Response
+
+```json
+{
+ "referralCode": "string"
+}
+```
+
+### Responses
+
+|Status|Meaning|Description|Schema|
+|---|---|---|---|
+|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Ok|[AffiliateReferralCodeResponse](#schemaaffiliatereferralcoderesponse)|
+
+
+
## GetAssetPositions
@@ -3742,6 +3818,26 @@ This operation does not require authentication
|freeCollateral|string|true|none|none|
|childSubaccounts|[[SubaccountResponseObject](#schemasubaccountresponseobject)]|true|none|none|
+## AffiliateReferralCodeResponse
+
+
+
+
+
+
+```json
+{
+ "referralCode": "string"
+}
+
+```
+
+### Properties
+
+|Name|Type|Required|Restrictions|Description|
+|---|---|---|---|---|
+|referralCode|string¦null|true|none|none|
+
## AssetPositionResponse
diff --git a/indexer/services/comlink/public/swagger.json b/indexer/services/comlink/public/swagger.json
index 089550239a9..baf60cfb99d 100644
--- a/indexer/services/comlink/public/swagger.json
+++ b/indexer/services/comlink/public/swagger.json
@@ -240,6 +240,19 @@
"type": "object",
"additionalProperties": false
},
+ "AffiliateReferralCodeResponse": {
+ "properties": {
+ "referralCode": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "referralCode"
+ ],
+ "type": "object",
+ "additionalProperties": false
+ },
"AssetPositionResponse": {
"properties": {
"positions": {
@@ -1520,6 +1533,34 @@
]
}
},
+ "/affiliate/referral_code": {
+ "get": {
+ "operationId": "GetReferralCode",
+ "responses": {
+ "200": {
+ "description": "Ok",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AffiliateReferralCodeResponse"
+ }
+ }
+ }
+ }
+ },
+ "security": [],
+ "parameters": [
+ {
+ "in": "query",
+ "name": "address",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ },
"/assetPositions": {
"get": {
"operationId": "GetAssetPositions",
diff --git a/indexer/services/comlink/src/controllers/api/index-v4.ts b/indexer/services/comlink/src/controllers/api/index-v4.ts
index d3e9f4f4120..af31fc6311d 100644
--- a/indexer/services/comlink/src/controllers/api/index-v4.ts
+++ b/indexer/services/comlink/src/controllers/api/index-v4.ts
@@ -1,6 +1,7 @@
import express from 'express';
import AddressesController from './v4/addresses-controller';
+import AffiliatesController from './v4/affiliates-controller';
import AssetPositionsController from './v4/asset-positions-controller';
import CandlesController from './v4/candles-controller';
import ComplianceController from './v4/compliance-controller';
@@ -26,6 +27,7 @@ import VaultController from './v4/vault-controller';
const router: express.Router = express.Router();
router.use('/addresses', AddressesController);
+router.use('/affiliates', AffiliatesController);
router.use('/assetPositions', AssetPositionsController);
router.use('/candles', CandlesController);
router.use('/fills', FillsController);
diff --git a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts
new file mode 100644
index 00000000000..e02aa0f3d51
--- /dev/null
+++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts
@@ -0,0 +1,72 @@
+import { stats } from '@dydxprotocol-indexer/base';
+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 { handleControllerError } from '../../../lib/helpers';
+import { rateLimiterMiddleware } from '../../../lib/rate-limit';
+import ExportResponseCodeStats from '../../../request-helpers/export-response-code-stats';
+import { AffiliateReferralCodeRequest, AffiliateReferralCodeResponse } from '../../../types';
+
+const router: express.Router = express.Router();
+const controllerName: string = 'affiliates-controller';
+
+@Route('affiliates')
+class AffiliatesController extends Controller {
+ @Get('/referral_code')
+ async getReferralCode(
+ @Query() address: string,
+ ): Promise {
+
+ // simulate a delay
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ return {
+ referralCode: `${address}-referral_code`,
+ };
+ }
+}
+
+router.get(
+ '/referral_code',
+ rateLimiterMiddleware(getReqRateLimiter),
+ ...checkSchema({
+ address: {
+ in: ['query'],
+ isString: true,
+ errorMessage: 'address must be a valid string',
+ },
+ }),
+ ExportResponseCodeStats({ controllerName }),
+ async (req: express.Request, res: express.Response) => {
+ const start: number = Date.now();
+ const {
+ address,
+ }: AffiliateReferralCodeRequest = matchedData(req) as AffiliateReferralCodeRequest;
+
+ try {
+ const controller: AffiliatesController = new AffiliatesController();
+ const response: AffiliateReferralCodeResponse = await controller.getReferralCode(address);
+ return res.send(response);
+ } catch (error) {
+ return handleControllerError(
+ 'AffiliatesController GET /referral_code',
+ 'Affiliates refferal code error',
+ error,
+ req,
+ res,
+ );
+ } finally {
+ stats.timing(
+ `${config.SERVICE_NAME}.${controllerName}.get_referral_code.timing`,
+ Date.now() - start,
+ );
+ }
+ },
+);
+
+export default router;
diff --git a/indexer/services/comlink/src/types.ts b/indexer/services/comlink/src/types.ts
index bf9db5c2f63..1715cddf8c6 100644
--- a/indexer/services/comlink/src/types.ts
+++ b/indexer/services/comlink/src/types.ts
@@ -666,3 +666,32 @@ export interface VaultPosition {
export interface MegavaultPositionResponse {
positions: VaultPosition[];
}
+
+/* ------- Affiliates Types ------- */
+export interface AffiliateReferralCodeRequest{
+ address: string
+}
+
+export interface AffiliateReferralCodeResponse {
+ referralCode: string | null,
+}
+
+export interface AffiliateAddressResponse {
+ address: string | null,
+}
+
+export interface AffiliateSnapshotResponse {
+ affiliateList: AffiliateSnapshotResponseObject[],
+ total: number,
+ currentOffset: number
+}
+
+export interface AffiliateSnapshotResponseObject {
+ affiliateAddress: string,
+ affiliateEarnings: number,
+ affiliateReferralCode: string,
+ affiliateReferredTrades: number,
+ affiliateTotalReferredFees: number,
+ affiliateReferredUsers: number,
+ affiliateReferredNetProtocolEarnings: number
+}