Skip to content

Commit

Permalink
fix(affiliates): [OTE-898] Refactor subaccount username generation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
teddyding authored Nov 14, 2024
1 parent de6d106 commit d1c64f6
Show file tree
Hide file tree
Showing 16 changed files with 625 additions and 128 deletions.
15 changes: 15 additions & 0 deletions indexer/packages/postgres/__tests__/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const invalidTicker: string = 'INVALID-INVALID';
export const dydxChain: string = 'dydx';
export const defaultAddress: string = 'dydx1n88uc38xhjgxzw9nwre4ep2c8ga4fjxc565lnf';
export const defaultAddress2: string = 'dydx1n88uc38xhjgxzw9nwre4ep2c8ga4fjxc575lnf';
export const defaultAddress3: string = 'dydx199tqg4wdlnu4qjlxchpd7seg454937hjrknju4';
export const blockedAddress: string = 'dydx1f9k5qldwmqrnwy8hcgp4fw6heuvszt35egvtx2';
// Vault address for vault id 0 was generated using
// script protocol/scripts/vault/get_vault.go
Expand Down Expand Up @@ -100,6 +101,20 @@ export const defaultSubaccount3: SubaccountCreateObject = {
updatedAtHeight: createdHeight,
};

export const defaultSubaccount2Num0: SubaccountCreateObject = {
address: defaultAddress2,
subaccountNumber: 0,
updatedAt: createdDateTime.toISO(),
updatedAtHeight: createdHeight,
};

export const defaultSubaccount3Num0: SubaccountCreateObject = {
address: defaultAddress3,
subaccountNumber: 0,
updatedAt: createdDateTime.toISO(),
updatedAtHeight: createdHeight,
};

// defaultWalletAddress belongs to defaultWallet2 and is different from defaultAddress
export const defaultSubaccountDefaultWalletAddress: SubaccountCreateObject = {
address: defaultWalletAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ import {
isolatedPerpetualMarket2,
isolatedSubaccount,
isolatedSubaccount2,
defaultSubaccount2Num0,
defaultSubaccount3Num0,
} from './constants';

export async function seedAdditionalSubaccounts() {
await Promise.all([
SubaccountTable.create(defaultSubaccount2Num0),
SubaccountTable.create(defaultSubaccount3Num0),
]);
}

export async function seedData() {
await Promise.all([
SubaccountTable.create(defaultSubaccount),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import { clearData, migrate, teardown } from '../../src/helpers/db-helpers';
import {
defaultSubaccountUsername,
defaultSubaccountUsername2,
defaultSubaccountWithAlternateAddress,
defaultWallet,
defaultWallet2,
duplicatedSubaccountUsername,
subaccountUsernameWithAlternativeAddress,
} from '../helpers/constants';
import { seedData } from '../helpers/mock-generators';
import { seedData, seedAdditionalSubaccounts } from '../helpers/mock-generators';

describe('SubaccountUsernames store', () => {
beforeEach(async () => {
await seedData();
await seedAdditionalSubaccounts();
});

beforeAll(async () => {
Expand Down Expand Up @@ -82,18 +82,17 @@ describe('SubaccountUsernames store', () => {
const subaccountLength = subaccounts.length;
await SubaccountUsernamesTable.create(defaultSubaccountUsername);
const subaccountIds: SubaccountsWithoutUsernamesResult[] = await
SubaccountUsernamesTable.getSubaccountsWithoutUsernames();
SubaccountUsernamesTable.getSubaccountZerosWithoutUsernames(1000);
expect(subaccountIds.length).toEqual(subaccountLength - 1);
});

it('Get username using address', async () => {
await Promise.all([
// Add two usernames for defaultWallet
// Add username for defaultWallet
SubaccountUsernamesTable.create(defaultSubaccountUsername),
SubaccountUsernamesTable.create(defaultSubaccountUsername2),
// Add one username for alternativeWallet
WalletTable.create(defaultWallet2),
SubaccountsTable.create(defaultSubaccountWithAlternateAddress),
SubaccountUsernamesTable.create(subaccountUsernameWithAlternativeAddress),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,24 @@ export async function findByUsername(
return (await baseQuery).find((subaccountUsername) => subaccountUsername.username === username);
}

export async function getSubaccountsWithoutUsernames(
export async function getSubaccountZerosWithoutUsernames(
limit: number,
options: Options = DEFAULT_POSTGRES_OPTIONS):
Promise<SubaccountsWithoutUsernamesResult[]> {
const queryString: string = `
SELECT id as "subaccountId"
SELECT id as "subaccountId", address
FROM subaccounts
WHERE subaccounts."subaccountNumber" = 0
EXCEPT
SELECT "subaccountId" FROM subaccount_usernames;
AND id NOT IN (
SELECT "subaccountId" FROM subaccount_usernames
)
ORDER BY address
LIMIT ?
`;

const result: {
rows: SubaccountsWithoutUsernamesResult[],
} = await rawQuery(queryString, options);
} = await rawQuery(queryString, { ...options, bindings: [limit] });

return result.rows;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export enum SubaccountUsernamesColumns {

export interface SubaccountsWithoutUsernamesResult {
subaccountId: string,
address: string,
}
12 changes: 12 additions & 0 deletions indexer/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class AffiliatesController extends Controller {
async getMetadata(
@Query() address: string,
): Promise<AffiliateMetadataResponse> {
const [walletRow, referredUserRows, subaccountRows] = await Promise.all([
const [walletRow, referredUserRows, subaccountZeroRows] = await Promise.all([
WalletTable.findById(address),
AffiliateReferredUsersTable.findByAffiliateAddress(address),
SubaccountTable.findAll(
Expand All @@ -65,11 +65,17 @@ class AffiliatesController extends Controller {
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) {
if (subaccountZeroRows.length === 0) {
// error logging will be performed by handleInternalServerError
throw new UnexpectedServerError(`Subaccount 0 not found for address ${address}`);
} else if (subaccountZeroRows.length > 1) {
logger.error({
at: 'affiliates-controller#snapshot',
message: `More than 1 username exist for address: ${address}`,
subaccountZeroRows,
});
}
const subaccountId = subaccountRows[0].id;
const subaccountId = subaccountZeroRows[0].id;

// Get subaccount0 username, which is the referral code
const usernameRows = await SubaccountUsernamesTable.findAll(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,45 @@
import { generateUsername } from '../../src/helpers/usernames-helper';
import {
generateUsernameForSubaccount,
} from '../../src/helpers/usernames-helper';

describe('usernames-helper', () => {
it('Check format of username', () => {
const username: string = generateUsername();
expect(username.match(/[A-Z]/g)).toHaveLength(2);
expect(username.match(/\d/g)).toHaveLength(3);
// check length is at the very minimum 7
expect(username.length).toBeGreaterThanOrEqual(7);
it('Check result and determinism of username username', () => {
const addresses = [
'dydx1gf4xlnpulkyex74asxxhg9ye05r28cxdd69s9u',
'dydx10fx7sy6ywd5senxae9dwytf8jxek3t2gcen2vs',
'dydx1t72ww7qzdx5rjlpp6cq0cqy09qlsjj7e4kpuyt',
'dydx1wau5mja7j7zdavtfq9lu7ejef05hm6ffenlcsn',
'dydx168pjt8rkru35239fsqvz7rzgeclakp49zx3aum',
'dydx1df84hz7y0dd3mrqcv3vrhw9wdttelul8edqmvp',
'dydx16h7p7f4dysrgtzptxx2gtpt5d8t834g9dj830z',
'dydx15u9tppy5e2pdndvlrvafxqhuurj9mnpdstzj6z',
];

const expectedUsernames = [
'CushyHand599',
'AmpleCube324',
'AwareFood215',
'LoudLand654',
'MossyStraw800',
'BoldGap392',
'ZoomEra454',
'WiryFern332',
];

for (let i = 0; i < addresses.length; i++) {
const address = addresses[i];
for (let j = 0; j < 10; j++) {
const names = new Set();
for (let k = 0; k < 10; k++) {
const username: string = generateUsernameForSubaccount(address, 0, k);
if (k === 0) {
expect(username).toEqual(expectedUsernames[i]);
}
names.add(username);
}
// for same address, difference nonce should result in different username
expect(names.size).toEqual(10);
}
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('subaccount-username-generator', () => {

beforeEach(async () => {
await testMocks.seedData();
await testMocks.seedAdditionalSubaccounts();
});

afterAll(async () => {
Expand Down
2 changes: 2 additions & 0 deletions indexer/services/roundtable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"lodash": "^4.17.21",
"luxon": "^3.0.1",
"redis": "2.8.0",
"seedrandom": "^3.0.5",
"uuid": "^8.3.2"
},
"devDependencies": {
Expand All @@ -41,6 +42,7 @@
"@types/luxon": "^3.0.0",
"@types/node": "^18.0.3",
"@types/redis": "2.8.27",
"@types/seedrandom": "^3.0.8",
"@types/uuid": "^8.3.4",
"jest": "^28.1.2",
"redis": "2.8.0",
Expand Down
5 changes: 4 additions & 1 deletion indexer/services/roundtable/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export const configSchema = {
DELETE_ZERO_PRICE_LEVELS_LOCK_MULTIPLIER: parseInteger({ default: 1 }),
UNCROSS_ORDERBOOK_LOCK_MULTIPLIER: parseInteger({ default: 1 }),
PNL_TICK_UPDATE_LOCK_MULTIPLIER: parseInteger({ default: 20 }),
SUBACCOUNT_USERNAME_GENERATOR_LOCK_MULTIPLIER: parseInteger({ default: 5 }),

// Maximum number of running tasks - set this equal to PG_POOL_MIN in .env, default is 2
MAX_CONCURRENT_RUNNING_TASKS: parseInteger({ default: 2 }),
Expand Down Expand Up @@ -211,7 +212,9 @@ export const configSchema = {

// Subaccount username generator
SUBACCOUNT_USERNAME_NUM_RANDOM_DIGITS: parseInteger({ default: 3 }),
SUBACCOUNT_USERNAME_MAX_LENGTH: parseInteger({ default: 13 }),
SUBACCOUNT_USERNAME_BATCH_SIZE: parseInteger({ default: 1000 }),
// number of attempts to generate username for a subaccount
ATTEMPT_PER_SUBACCOUNT: parseInteger({ default: 3 }),
};

export default parseSchema(configSchema);
Loading

0 comments on commit d1c64f6

Please sign in to comment.