Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OTE-760] implement comlink affiliate metadata endpoint #2243

Merged
merged 8 commits into from
Sep 13, 2024
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
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
3 changes: 3 additions & 0 deletions indexer/services/comlink/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you verify volume is stored in dollars and not quote quantums?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the quantum conversion is made before writing to fills db

    fill_amount = dydx_trim_scale(dydx_from_jsonlib_long(event_data->'fillAmount') *
                                  power(10, perpetual_market_record."atomicResolution")::numeric);
    maker_price = dydx_trim_scale(dydx_from_jsonlib_long(maker_order->'subticks') *
                                  power(10, perpetual_market_record."quantumConversionExponent" +
                                                     asset_record."atomicResolution" -
                                                     perpetual_market_record."atomicResolution")::numeric);

This screenshot also suggests it is converted

image

};

////////////////////////////////////////////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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';
Expand All @@ -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<AffiliateMetadataResponse> {
// 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of doing multiple awaits, can we do promise.All instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im not too famliar with ts, but the output of these functions is processed (for errors) and then used in subsequent functions. Would this be suitable for promise.all? My understanding is that promise.all should not be used for such cases?

Copy link
Contributor

@affanv14 affanv14 Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linked the wrong line
but cant we combine


const walletRow = await WalletTable.findById(address);
const referredUserRows = await AffiliateReferredUsersTable.findByAffiliateAddress(address);
    const subaccountRows = await SubaccountTable.findAll(
      {
        address,
        subaccountNumber: 0,
      },
      [],
    );

into one promise.all?
since all of these are independent and make db queries

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do!

{
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,
};
}

Expand Down
Loading