Skip to content

Commit

Permalink
[OTE-751] create affiliate referred users table (#2177)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerryfan01234 authored Aug 30, 2024
1 parent 1b65466 commit ffde811
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 0 deletions.
8 changes: 8 additions & 0 deletions indexer/packages/postgres/__tests__/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as TradingRewardAggregationTable from '../../src/stores/trading-reward-
import * as TransactionTable from '../../src/stores/transaction-table';
import * as TransferTable from '../../src/stores/transfer-table';
import {
AffiliateReferredUsersCreateObject,
AssetCreateObject,
AssetPositionCreateObject,
BlockCreateObject,
Expand Down Expand Up @@ -942,6 +943,13 @@ export const defaultLeaderboardPnlOneDayToUpsert: LeaderboardPnlCreateObject = {
rank: 1,
};

// ============== Affiliate referred users data ==============
export const defaultAffiliateReferredUser: AffiliateReferredUsersCreateObject = {
affiliateAddress: defaultAddress,
refereeAddress: defaultAddress2,
referredAtBlock: 1,
};

// ============== Persistent cache Data ==============

export const defaultKV: PersistentCacheCreateObject = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { AffiliateReferredUserFromDatabase, AffiliateReferredUsersCreateObject } from '../../src/types';
import { clearData, migrate, teardown } from '../../src/helpers/db-helpers';
import { defaultAffiliateReferredUser } from '../helpers/constants';
import * as AffiliateReferredUsersTable from '../../src/stores/affiliate-referred-users-table';

describe('AffiliateReferredUsers store', () => {
beforeAll(async () => {
await migrate();
});

afterEach(async () => {
await clearData();
});

afterAll(async () => {
await teardown();
});

it('Successfully creates affiliate referee pairs', async () => {
await AffiliateReferredUsersTable.create(defaultAffiliateReferredUser);
await AffiliateReferredUsersTable.create({
...defaultAffiliateReferredUser,
refereeAddress: 'fake_address',
});
});

it('Should not allow duplicate refree address', async () => {
await AffiliateReferredUsersTable.create(defaultAffiliateReferredUser);

// Second creation should fail due to the duplicate refereeAddress
await expect(
AffiliateReferredUsersTable.create({
...defaultAffiliateReferredUser,
affiliateAddress: 'another_affiliate_address',
}),
).rejects.toThrow();
});

it('Successfully finds all entries', async () => {
const entry1: AffiliateReferredUsersCreateObject = {
...defaultAffiliateReferredUser,
refereeAddress: 'referee_address1',
};
const entry2: AffiliateReferredUsersCreateObject = {
...defaultAffiliateReferredUser,
affiliateAddress: 'affiliate_address1',
refereeAddress: 'referee_address2',
};

await Promise.all([
AffiliateReferredUsersTable.create(defaultAffiliateReferredUser),
AffiliateReferredUsersTable.create(entry1),
AffiliateReferredUsersTable.create(entry2),
]);

const entries: AffiliateReferredUserFromDatabase[] = await AffiliateReferredUsersTable.findAll(
{},
[],
{ readReplica: true },
);

expect(entries.length).toEqual(3);
expect(entries).toEqual(
expect.arrayContaining([
expect.objectContaining(defaultAffiliateReferredUser),
expect.objectContaining(entry1),
expect.objectContaining(entry2),
]),
);
});

it('Successfully finds entries by affiliate address', async () => {
const entry1: AffiliateReferredUsersCreateObject = {
affiliateAddress: 'affiliate_address1',
refereeAddress: 'referee_address1',
referredAtBlock: 1,
};
const entry2: AffiliateReferredUsersCreateObject = {
affiliateAddress: 'affiliate_address1',
refereeAddress: 'referee_address2',
referredAtBlock: 20,
};

await AffiliateReferredUsersTable.create(entry1);
await AffiliateReferredUsersTable.create(entry2);

const entries: AffiliateReferredUserFromDatabase[] | undefined = await AffiliateReferredUsersTable.findByAffiliateAddress('affiliate_address1');

if (entries) {
expect(entries.length).toEqual(2);
expect(entries).toEqual(
expect.arrayContaining([
expect.objectContaining(entry1),
expect.objectContaining(entry2),
]),
);
} else {
throw new Error('findByAffiliateAddress returned undefined, expected an array');
}
});

it('Successfully finds entry by referee address', async () => {
const entry1: AffiliateReferredUsersCreateObject = {
affiliateAddress: 'affiliate_address1',
refereeAddress: 'referee_address1',
referredAtBlock: 1,
};
const entry2: AffiliateReferredUsersCreateObject = {
affiliateAddress: 'affiliate_address1',
refereeAddress: 'referee_address2',
referredAtBlock: 20,
};

await AffiliateReferredUsersTable.create(entry1);
await AffiliateReferredUsersTable.create(entry2);

const entry: AffiliateReferredUserFromDatabase | undefined = await AffiliateReferredUsersTable.findByRefereeAddress('referee_address1');

expect(entry).toEqual(expect.objectContaining(entry1));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Knex from 'knex';

export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('affiliate_referred_users', (table) => {
table.string('refereeAddress').primary().notNullable();
table.string('affiliateAddress').notNullable();
table.integer('referredAtBlock').notNullable();

// Index on affiliateAddress for faster queries
table.index(['affiliateAddress']);
});
}

export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('affiliate_referred_users');
}
1 change: 1 addition & 0 deletions indexer/packages/postgres/src/helpers/db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const layer1Tables = [
'trading_rewards',
'trading_reward_aggregations',
'compliance_status',
'affiliate_referred_users',
'persistent_cache',
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import BaseModel from './base-model';

export default class AffiliateReferredUsersModel extends BaseModel {
static get tableName() {
return 'affiliate_referred_users';
}

static get idColumn() {
return 'refereeAddress';
}

static get jsonSchema() {
return {
type: 'object',
required: [
'affiliateAddress',
'refereeAddress',
'referredAtBlock',
],
properties: {
affiliateAddress: { type: 'string' },
refereeAddress: { type: 'string' },
referredAtBlock: { type: 'integer' },
},
};
}

/**
* A mapping from column name to JSON conversion expected.
* See getSqlConversionForDydxModelTypes for valid conversions.
*
* TODO(IND-239): Ensure that jsonSchema() / sqlToJsonConversions() / model fields match.
*/
static get sqlToJsonConversions() {
return {
affiliateAddress: 'string',
refereeAddress: 'string',
referredAtBlock: 'integer',
};
}

affiliateAddress!: string;

refereeAddress!: string;

referredAtBlock!: number;
}
110 changes: 110 additions & 0 deletions indexer/packages/postgres/src/stores/affiliate-referred-users-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { QueryBuilder } from 'objection';

import { DEFAULT_POSTGRES_OPTIONS } from '../constants';
import { setupBaseQuery, verifyAllRequiredFields } from '../helpers/stores-helpers';
import Transaction from '../helpers/transaction';
import AffiliateReferredUsersModel from '../models/affiliate-referred-users-model';
import {
Options,
Ordering,
QueryableField,
QueryConfig,
AffiliateReferredUsersColumns,
AffiliateReferredUsersCreateObject,
AffiliateReferredUserFromDatabase,
AffiliateReferredUsersQueryConfig,
} from '../types';

export async function findAll(
{
affiliateAddress,
refereeAddress,
limit,
}: AffiliateReferredUsersQueryConfig,
requiredFields: QueryableField[],
options: Options = DEFAULT_POSTGRES_OPTIONS,
): Promise<AffiliateReferredUserFromDatabase[]> {
verifyAllRequiredFields(
{
affiliateAddress,
refereeAddress,
limit,
} as QueryConfig,
requiredFields,
);

// splitting the line after = does not work because it is reformatted to one line by eslint
// eslint-disable-next-line max-len
let baseQuery: QueryBuilder<AffiliateReferredUsersModel> = setupBaseQuery<AffiliateReferredUsersModel>(
AffiliateReferredUsersModel,
options,
);

if (affiliateAddress) {
baseQuery = baseQuery.where(AffiliateReferredUsersColumns.affiliateAddress, affiliateAddress);
}

if (refereeAddress) {
baseQuery = baseQuery.where(AffiliateReferredUsersColumns.refereeAddress, refereeAddress);
}

if (options.orderBy !== undefined) {
for (const [column, order] of options.orderBy) {
baseQuery = baseQuery.orderBy(
column,
order,
);
}
} else {
baseQuery = baseQuery.orderBy(
AffiliateReferredUsersColumns.referredAtBlock,
Ordering.ASC,
);
}

if (limit) {
baseQuery = baseQuery.limit(limit);
}

return baseQuery.returning('*');
}

export async function create(
entryToCreate: AffiliateReferredUsersCreateObject,
options: Options = { txId: undefined },
): Promise<AffiliateReferredUserFromDatabase> {
return AffiliateReferredUsersModel.query(
Transaction.get(options.txId),
).insert(entryToCreate).returning('*');
}

export async function findByAffiliateAddress(
address: string,
options: Options = DEFAULT_POSTGRES_OPTIONS,
): Promise<AffiliateReferredUserFromDatabase[] | undefined> {
// splitting the line after = does not work because it is reformatted to one line by eslint
// eslint-disable-next-line max-len
const baseQuery: QueryBuilder<AffiliateReferredUsersModel> = setupBaseQuery<AffiliateReferredUsersModel>(
AffiliateReferredUsersModel,
options,
);
return baseQuery
.where('affiliateAddress', address)
.returning('*');
}

export async function findByRefereeAddress(
address: string,
options: Options = DEFAULT_POSTGRES_OPTIONS,
): Promise<AffiliateReferredUserFromDatabase | undefined> {
// splitting the line after = does not work because it is reformatted to one line by eslint
// eslint-disable-next-line max-len
const baseQuery: QueryBuilder<AffiliateReferredUsersModel> = setupBaseQuery<AffiliateReferredUsersModel>(
AffiliateReferredUsersModel,
options,
);
return baseQuery
.where('refereeAddress', address)
.returning('*')
.first(); // should only be one since refereeAddress is primary key
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export async function upsert(
// should only ever be one key value pair
return kvs[0];
}

export async function findById(
kv: string,
options: Options = DEFAULT_POSTGRES_OPTIONS,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface AffiliateReferredUsersCreateObject {
affiliateAddress: string,
refereeAddress: string,
referredAtBlock: number,
}

export enum AffiliateReferredUsersColumns {
affiliateAddress = 'affiliateAddress',
refereeAddress = 'refereeAddress',
referredAtBlock = 'referredAtBlock',
}
6 changes: 6 additions & 0 deletions indexer/packages/postgres/src/types/db-model-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ export interface PersistentCacheFromDatabase {
value: string,
}

export interface AffiliateReferredUserFromDatabase {
affiliateAddress: string,
refereeAddress: string,
referredAtBlock: number,
}

export type SubaccountAssetNetTransferMap = { [subaccountId: string]:
{ [assetId: string]: string }, };
export type SubaccountToPerpetualPositionsMap = { [subaccountId: string]:
Expand Down
1 change: 1 addition & 0 deletions indexer/packages/postgres/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ export * from './trading-reward-aggregation-types';
export * from './pagination-types';
export * from './subaccount-usernames-types';
export * from './leaderboard-pnl-types';
export * from './affiliate-referred-users-types';
export * from './persistent-cache-types';
export { PositionSide } from './position-types';
7 changes: 7 additions & 0 deletions indexer/packages/postgres/src/types/query-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export enum QueryableField {
USERNAME = 'username',
TIMESPAN = 'timeSpan',
RANK = 'rank',
AFFILIATE_ADDRESS = 'affiliateAddress',
REFEREE_ADDRESS = 'refereeAddress',
KEY = 'key',
IS_WHITELIST_AFFILIATE = 'isWhitelistAffiliate',
}
Expand Down Expand Up @@ -319,6 +321,11 @@ export interface TradingRewardAggregationQueryConfig extends QueryConfig {
[QueryableField.STARTED_AT_HEIGHT_BEFORE_OR_AT]?: string,
}

export interface AffiliateReferredUsersQueryConfig extends QueryConfig {
[QueryableField.AFFILIATE_ADDRESS]?: string[],
[QueryableField.REFEREE_ADDRESS]?: string[],
}

export interface LeaderboardPnlQueryConfig extends QueryConfig {
[QueryableField.ADDRESS]?: string[],
[QueryableField.TIMESPAN]?: string[],
Expand Down

0 comments on commit ffde811

Please sign in to comment.