diff --git a/indexer/packages/postgres/__tests__/helpers/constants.ts b/indexer/packages/postgres/__tests__/helpers/constants.ts index 5de24209bd..1de37ba7ac 100644 --- a/indexer/packages/postgres/__tests__/helpers/constants.ts +++ b/indexer/packages/postgres/__tests__/helpers/constants.ts @@ -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 @@ -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, diff --git a/indexer/packages/postgres/__tests__/helpers/mock-generators.ts b/indexer/packages/postgres/__tests__/helpers/mock-generators.ts index 34d9ada9e2..ea785b2944 100644 --- a/indexer/packages/postgres/__tests__/helpers/mock-generators.ts +++ b/indexer/packages/postgres/__tests__/helpers/mock-generators.ts @@ -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), diff --git a/indexer/packages/postgres/__tests__/stores/subaccount-usernames-table.test.ts b/indexer/packages/postgres/__tests__/stores/subaccount-usernames-table.test.ts index c069df9f59..71db35e943 100644 --- a/indexer/packages/postgres/__tests__/stores/subaccount-usernames-table.test.ts +++ b/indexer/packages/postgres/__tests__/stores/subaccount-usernames-table.test.ts @@ -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 () => { @@ -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), ]); diff --git a/indexer/packages/postgres/src/stores/subaccount-usernames-table.ts b/indexer/packages/postgres/src/stores/subaccount-usernames-table.ts index 72a894ce58..d03e91afbd 100644 --- a/indexer/packages/postgres/src/stores/subaccount-usernames-table.ts +++ b/indexer/packages/postgres/src/stores/subaccount-usernames-table.ts @@ -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 { 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; } diff --git a/indexer/packages/postgres/src/types/subaccount-usernames-types.ts b/indexer/packages/postgres/src/types/subaccount-usernames-types.ts index e6502776ca..c0b0eb25f0 100644 --- a/indexer/packages/postgres/src/types/subaccount-usernames-types.ts +++ b/indexer/packages/postgres/src/types/subaccount-usernames-types.ts @@ -12,4 +12,5 @@ export enum SubaccountUsernamesColumns { export interface SubaccountsWithoutUsernamesResult { subaccountId: string, + address: string, } diff --git a/indexer/pnpm-lock.yaml b/indexer/pnpm-lock.yaml index 98acd5cf3f..dd45073966 100644 --- a/indexer/pnpm-lock.yaml +++ b/indexer/pnpm-lock.yaml @@ -646,6 +646,7 @@ importers: '@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 aws-sdk: ^2.1399.0 big.js: ^6.2.1 @@ -656,6 +657,7 @@ importers: lodash: ^4.17.21 luxon: ^3.0.1 redis: 2.8.0 + seedrandom: ^3.0.5 ts-node: ^10.8.2 tsconfig-paths: ^4.0.0 typescript: ^4.7.4 @@ -676,6 +678,7 @@ importers: lodash: 4.17.21 luxon: 3.0.1 redis: 2.8.0 + seedrandom: 3.0.5 uuid: 8.3.2 devDependencies: '@dydxprotocol-indexer/dev': link:../../packages/dev @@ -685,6 +688,7 @@ importers: '@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_250642e41d506bccecc9f35ad915bcb5 ts-node: 10.8.2_2dd5d46eecda2aef953638919121af58 @@ -7095,6 +7099,10 @@ packages: '@types/node': 18.0.3 dev: true + /@types/seedrandom/3.0.8: + resolution: {integrity: sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==} + dev: true + /@types/send/0.17.4: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: @@ -14203,6 +14211,10 @@ packages: resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} dev: false + /seedrandom/3.0.5: + resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} + dev: false + /semver/5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true 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 c87ba90c09..9528aaec04 100644 --- a/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts +++ b/indexer/services/comlink/src/controllers/api/v4/affiliates-controller.ts @@ -43,7 +43,7 @@ class AffiliatesController extends Controller { async getMetadata( @Query() address: string, ): Promise { - const [walletRow, referredUserRows, subaccountRows] = await Promise.all([ + const [walletRow, referredUserRows, subaccountZeroRows] = await Promise.all([ WalletTable.findById(address), AffiliateReferredUsersTable.findByAffiliateAddress(address), SubaccountTable.findAll( @@ -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( diff --git a/indexer/services/roundtable/__tests__/helpers/usernames-helper.test.ts b/indexer/services/roundtable/__tests__/helpers/usernames-helper.test.ts index 38c5bb7cb1..49e5fdec1f 100644 --- a/indexer/services/roundtable/__tests__/helpers/usernames-helper.test.ts +++ b/indexer/services/roundtable/__tests__/helpers/usernames-helper.test.ts @@ -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); + } + } }); }); diff --git a/indexer/services/roundtable/__tests__/tasks/subaccount-username-generator.test.ts b/indexer/services/roundtable/__tests__/tasks/subaccount-username-generator.test.ts index acd52fe0b1..1a7ce0dfa1 100644 --- a/indexer/services/roundtable/__tests__/tasks/subaccount-username-generator.test.ts +++ b/indexer/services/roundtable/__tests__/tasks/subaccount-username-generator.test.ts @@ -18,6 +18,7 @@ describe('subaccount-username-generator', () => { beforeEach(async () => { await testMocks.seedData(); + await testMocks.seedAdditionalSubaccounts(); }); afterAll(async () => { diff --git a/indexer/services/roundtable/package.json b/indexer/services/roundtable/package.json index ad23413ca1..eafa83be59 100644 --- a/indexer/services/roundtable/package.json +++ b/indexer/services/roundtable/package.json @@ -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": { @@ -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", diff --git a/indexer/services/roundtable/src/config.ts b/indexer/services/roundtable/src/config.ts index bdc8877e5c..1666898a30 100644 --- a/indexer/services/roundtable/src/config.ts +++ b/indexer/services/roundtable/src/config.ts @@ -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 }), @@ -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); diff --git a/indexer/services/roundtable/src/helpers/adjectives.json b/indexer/services/roundtable/src/helpers/adjectives.json index c9873f0aaf..d5bace1a2b 100644 --- a/indexer/services/roundtable/src/helpers/adjectives.json +++ b/indexer/services/roundtable/src/helpers/adjectives.json @@ -1,16 +1,18 @@ [ - "able", "ace", "acid", + "active", "airy", "alert", "all", - "alpha", "aloof", + "alpha", "amber", "ample", + "amused", "apt", "arcane", + "ardent", "arid", "artsy", "ashy", @@ -23,38 +25,34 @@ "beefy", "beige", "best", - "big", - "blah", - "bland", "blocky", "blue", - "bogus", "bold", "bony", - "bossy", "bouncy", "brave", "breezy", "brief", + "bright", "brisk", "broad", "bubbly", "bumpy", "busy", - "cagey", "calm", - "campy", "candid", "canny", + "caring", "casual", + "catchy", "chaotic", - "cheap", "cheeky", "cheery", "chief", "chilly", - "chubby", + "chirpy", "civil", + "classy", "clean", "clear", "close", @@ -64,11 +62,12 @@ "copper", "coral", "corny", + "cosmic", "cozy", "crafty", - "cranky", "crisp", "crusty", + "curious", "curly", "curt", "cushy", @@ -83,23 +82,28 @@ "dinky", "dizzy", "domed", + "dope", "dotty", + "dreamy", + "driven", "dry", "dull", "dusty", - "eager", "early", "earthy", "easy", + "elated", "elite", "empty", - "equal", + "epic", "exact", "exotic", "expert", "extra", "faded", "fair", + "famed", + "famous", "fancy", "far", "fast", @@ -107,9 +111,9 @@ "fine", "firm", "fishy", + "fit", "flaky", - "flat", - "flimsy", + "fluffy", "fresh", "frilly", "frosty", @@ -117,34 +121,37 @@ "full", "funny", "fuzzy", - "gauche", - "gaudy", + "gentle", "giant", "giddy", + "giving", "glad", "gnarly", + "golden", "gonzo", "good", "goofy", "grand", "great", "green", - "grim", + "grey", "gummy", "handy", "happy", - "hard", "hardy", "harsh", "hasty", "hazy", - "heavy", + "hearty", "hefty", "high", "hilly", "hip", + "holy", + "honest", "hot", "huge", + "humble", "humid", "husky", "icky", @@ -157,15 +164,18 @@ "jade", "jazzy", "jolly", + "jovial", + "joyful", "jumpy", + "just", "keen", "khaki", "kind", - "lame", "large", "last", "late", "lavish", + "lawful", "lax", "lazy", "leafy", @@ -181,6 +191,7 @@ "long", "loud", "lousy", + "loved", "low", "loyal", "lucid", @@ -194,14 +205,17 @@ "matey", "mauve", "meek", + "mellow", "merry", "mild", "misty", - "moist", + "modern", + "modest", "moral", "mossy", - "mute", "mushy", + "mute", + "mystic", "naive", "near", "neat", @@ -216,8 +230,8 @@ "obtuse", "odd", "olden", - "open", "opal", + "open", "other", "oval", "pale", @@ -228,29 +242,37 @@ "petty", "plain", "plump", + "poised", "polite", "poor", + "prime", + "proper", "proud", "pure", + "purple", "quick", "quiet", + "radiant", "rapid", "rare", "raw", "ready", "real", "red", + "regal", "retro", "rich", "rigid", "ripe", + "robust", "roomy", "rosy", "rough", "round", - "rude", "royal", + "rude", "rusty", + "sacred", "sad", "safe", "salty", @@ -259,7 +281,9 @@ "sane", "sassy", "scaly", + "secure", "sepia", + "serene", "shaky", "sharp", "shiny", @@ -269,13 +293,14 @@ "silly", "slack", "sleek", + "sleepy", "slick", - "slim", "slow", "small", "smart", - "smug", + "smiley", "smooth", + "smug", "snappy", "snug", "soft", @@ -293,6 +318,9 @@ "still", "stout", "stuffy", + "sturdy", + "suave", + "subtle", "sunny", "super", "sweet", @@ -306,25 +334,23 @@ "tasty", "taupe", "teal", - "tepid", "tense", - "thick", + "tepid", "thin", "tidy", "tiny", "tired", "tough", + "trendy", "trim", "true", - "tubby", "twisty", - "udder", "ultra", "umber", "unique", "unruly", "untidy", - "unzip", + "upbeat", "upper", "urban", "used", @@ -336,9 +362,9 @@ "vexed", "vital", "vivid", + "vocal", "wacky", "warm", - "weak", "weary", "wide", "windy", @@ -348,12 +374,10 @@ "witty", "wooly", "woozy", - "yappy", "yummy", "zany", "zero", "zesty", - "zingy", "zippy", "zoom" ] diff --git a/indexer/services/roundtable/src/helpers/nouns.json b/indexer/services/roundtable/src/helpers/nouns.json index 63afed6d28..c923cb65c5 100644 --- a/indexer/services/roundtable/src/helpers/nouns.json +++ b/indexer/services/roundtable/src/helpers/nouns.json @@ -1,240 +1,584 @@ [ + "accent", + "access", "ace", "acorn", "act", + "actor", + "advice", + "agenda", + "agent", "air", + "alarm", + "alley", + "altar", + "amber", + "amount", + "angle", "ant", + "anthem", "apple", + "april", + "arch", + "area", + "argue", "arm", + "army", + "arrow", "art", + "artist", + "asia", "atom", + "author", + "award", "axe", + "baby", + "back", + "badge", + "badger", "bag", + "baker", "ball", + "ballet", + "banana", "band", "bank", - "bark", + "bar", "base", - "bath", - "bead", - "beam", - "bean", + "beach", "bear", + "beat", + "beaver", "bed", "bee", + "beef", + "beer", "bell", "belt", + "bench", + "berry", + "best", + "bet", + "bike", + "bill", "bird", + "bite", "blade", + "blast", + "block", "boat", + "bond", "bone", "book", "boot", - "bowl", + "born", + "boss", + "bow", "box", + "brain", + "brake", + "brand", + "brass", "bread", + "break", "brick", - "brush", + "brief", + "buffalo", "bulb", + "bull", + "bump", "bus", + "bush", + "button", + "buy", + "cab", "cake", + "call", + "calm", + "camel", + "camp", "can", + "candle", "cap", "car", "card", + "care", + "cargo", "cart", + "case", + "cash", "cat", + "catch", + "cause", "cave", + "cell", + "chain", "chair", "chalk", + "chance", "chart", + "chat", + "check", "cheese", + "chef", "chess", + "chip", + "chop", + "city", + "claim", + "clap", + "class", + "claw", "clay", + "click", + "climb", "clock", "cloud", + "club", + "coach", + "coal", + "coast", "coat", + "code", "coin", "comb", + "cone", "cook", + "cool", + "coral", "cord", "cork", "corn", + "cost", + "couch", + "count", + "cover", "cow", "crab", + "crash", + "creek", + "crop", + "cross", "crow", + "crowd", "crown", + "cry", "cube", "cup", + "curb", + "curl", + "cycle", + "dance", + "day", + "dazzle", + "deal", + "deer", "desk", + "dial", + "diet", + "dig", + "dime", + "dingo", "dish", "dog", + "doll", + "dolphin", "door", "dot", - "drum", + "dove", + "draft", + "drag", + "draw", + "dream", + "dress", + "drip", + "drive", "duck", "dust", "ear", + "earth", + "eat", + "edge", "egg", + "elbow", + "elf", + "engine", + "era", "eye", - "face", + "fact", "fan", "farm", + "fax", + "feat", "fern", + "ferret", + "fiber", + "file", + "fill", + "film", "fire", "fish", + "fist", + "fix", "flag", + "flame", + "flat", "flute", + "fly", "fog", + "fold", "food", - "foot", "fork", + "form", "fox", + "frame", "frog", "fruit", + "fuel", + "fun", "game", + "gap", "gate", "gem", "gift", + "giraffe", "glass", "glove", + "glue", "goal", "goat", "gold", + "golf", + "good", + "grab", "grape", + "graph", "grass", + "gray", + "grip", + "grizzly", + "gum", + "guy", + "hall", + "hand", "hat", - "head", - "heart", + "hawk", + "heat", + "hedgie", + "heel", + "hen", "hill", + "hippo", + "hit", + "hold", + "hole", "home", + "honey", + "hood", "hook", + "hop", "horn", "horse", - "hose", + "host", + "hour", "house", + "hug", + "hull", + "humor", + "hut", + "hyena", + "ibex", "ice", + "idea", + "inch", "ink", + "inn", "iron", + "jaguar", + "jam", "jar", "jet", + "job", + "jog", + "joy", + "jump", "key", "kite", - "knee", - "knife", + "koala", + "lab", + "lace", + "lake", + "lamb", "lamp", + "land", + "law", + "layer", "leaf", + "lean", "leg", + "lemon", + "lemur", "lens", + "lid", + "life", + "lift", + "liger", + "light", + "lily", + "lime", "line", "lion", "lip", + "llama", + "load", "lock", "log", - "loop", + "look", + "lot", + "love", + "luck", "mail", "map", + "march", + "mark", "mask", "mat", - "milk", + "meal", + "melon", + "melt", + "meme", + "menu", + "mess", + "meter", + "mile", + "mill", + "mine", + "mink", "mint", "mist", + "mode", "moon", - "moss", - "moth", - "mouse", + "moose", + "mop", + "mud", "mug", "nail", - "nest", + "name", + "nap", "net", - "nose", + "news", + "ninja", "note", - "nut", - "oar", + "nugget", + "oak", + "ocean", + "oil", + "onion", + "open", + "opera", + "orange", + "orbit", + "otter", "owl", + "ox", + "pad", "page", + "paint", "pan", + "panda", + "park", + "pass", + "path", + "paw", "pea", + "peace", "peach", "pear", "pen", - "pig", + "pencil", + "penny", + "pet", + "phone", + "photo", + "piano", + "pick", + "pie", "pin", "pipe", + "pit", "plan", - "plane", "plant", "plate", "plum", - "pool", - "pot", + "pod", + "poem", + "poet", + "pond", + "pop", + "popcorn", + "port", + "post", + "pound", + "print", + "pump", + "pup", + "quiz", + "rail", "rain", - "rat", + "ramp", + "ray", + "reef", + "rice", + "ride", "ring", + "river", "road", "rock", + "rod", + "roll", "roof", "room", "root", "rope", "rose", - "rug", + "row", + "rule", + "safe", "sail", + "salad", "salt", "sand", - "sea", + "saw", + "scale", + "scarf", + "seal", "seed", + "set", + "sew", + "shade", + "sheep", + "shell", "ship", "shoe", "shop", + "show", + "side", "sign", "silk", - "sink", + "sir", + "sit", + "ski", "sky", "sled", + "slip", + "slow", + "smile", + "smoke", + "snap", "snow", "soap", "sock", "sofa", + "soil", + "song", + "soul", + "sound", "soup", + "soy", + "space", + "spark", + "speed", + "spell", + "spin", + "spoon", + "spot", + "spring", + "spy", + "stack", + "stage", "star", - "stem", "step", "stew", - "stove", + "store", + "storm", + "straw", + "sum", + "summer", "sun", "swan", - "table", + "tag", "tail", + "tan", + "tank", + "tap", + "task", "tea", + "team", + "tear", "tent", + "thorn", + "thread", "tie", - "tire", + "tiger", + "time", + "tin", + "tip", "toe", - "tomb", "tool", - "tooth", "top", "toy", + "track", + "train", + "trap", "tree", + "trick", + "trip", + "truck", "tub", + "tug", + "tune", + "turtle", + "twig", + "twin", + "twist", + "type", + "unicorn", + "unit", + "van", "vase", + "veil", + "vein", "vest", - "wall", + "vine", + "vodka", + "voice", + "vote", + "war", "wave", - "weed", + "wax", + "web", "well", + "whale", "wheel", + "whip", + "win", + "wind", + "wine", "wing", + "winter", "wire", + "wish", "wolf", + "wombat", "wood", "wool", - "worm", - "yard", + "word", + "work", + "wrap", "yarn", + "year", + "yolk", "yoyo", "zebra", - "zero", "zoo" ] diff --git a/indexer/services/roundtable/src/helpers/usernames-helper.ts b/indexer/services/roundtable/src/helpers/usernames-helper.ts index e668d46a2b..798790733b 100644 --- a/indexer/services/roundtable/src/helpers/usernames-helper.ts +++ b/indexer/services/roundtable/src/helpers/usernames-helper.ts @@ -1,13 +1,18 @@ -import { randomInt } from 'crypto'; +import seedrandom from 'seedrandom'; import config from '../config'; import adjectives from './adjectives.json'; import nouns from './nouns.json'; -export function generateUsername(): string { - const randomAdjective: string = adjectives[randomInt(0, adjectives.length)]; - const randomNoun: string = nouns[randomInt(0, nouns.length)]; - const randomNumber: string = randomInt(0, 1000).toString().padStart( +export function generateUsernameForSubaccount( + subaccountId: string, + subaccountNum: number, + nounce: number = 0, // incremented in case of collision +): string { + const rng = seedrandom(`${subaccountId}/${subaccountNum}/${nounce}`); + const randomAdjective: string = adjectives[Math.floor(rng() * adjectives.length)]; + const randomNoun: string = nouns[Math.floor(rng() * nouns.length)]; + const randomNumber: string = Math.floor(rng() * 1000).toString().padStart( config.SUBACCOUNT_USERNAME_NUM_RANDOM_DIGITS, '0'); const capitalizedAdjective: string = randomAdjective.charAt( diff --git a/indexer/services/roundtable/src/index.ts b/indexer/services/roundtable/src/index.ts index f52903ac19..22f1697e67 100644 --- a/indexer/services/roundtable/src/index.ts +++ b/indexer/services/roundtable/src/index.ts @@ -202,6 +202,7 @@ async function start(): Promise { subaccountUsernameGeneratorTask, 'subaccount_username_generator', config.LOOPS_INTERVAL_MS_SUBACCOUNT_USERNAME_GENERATOR, + config.SUBACCOUNT_USERNAME_GENERATOR_LOCK_MULTIPLIER, ); } diff --git a/indexer/services/roundtable/src/tasks/subaccount-username-generator.ts b/indexer/services/roundtable/src/tasks/subaccount-username-generator.ts index 819afa5bfd..9c3d7d46ef 100644 --- a/indexer/services/roundtable/src/tasks/subaccount-username-generator.ts +++ b/indexer/services/roundtable/src/tasks/subaccount-username-generator.ts @@ -3,37 +3,74 @@ import { SubaccountUsernamesTable, SubaccountsWithoutUsernamesResult, } from '@dydxprotocol-indexer/postgres'; +import _ from 'lodash'; import config from '../config'; -import { generateUsername } from '../helpers/usernames-helper'; +import { generateUsernameForSubaccount } from '../helpers/usernames-helper'; export default async function runTask(): Promise { - const subaccounts: + const subaccountZerosWithoutUsername: SubaccountsWithoutUsernamesResult[] = await - SubaccountUsernamesTable.getSubaccountsWithoutUsernames(); - for (const subaccount of subaccounts) { - const username: string = generateUsername(); - try { - // if insert fails, try it in the next roundtable cycle - // There are roughly ~87.5 million possible usernames - // so the chance of a collision is very low - await SubaccountUsernamesTable.create({ - username, - subaccountId: subaccount.subaccountId, - }); - } catch (e) { - if (e instanceof Error && e.name === 'UniqueViolationError') { - stats.increment( - `${config.SERVICE_NAME}.subaccount-username-generator.collision`, 1); - } else { - logger.error({ - at: 'subaccount-username-generator#runTask', - message: 'Failed to insert username for subaccount', - subaccountId: subaccount.subaccountId, + SubaccountUsernamesTable.getSubaccountZerosWithoutUsernames( + config.SUBACCOUNT_USERNAME_BATCH_SIZE, + ); + let successCount: number = 0; + for (const subaccount of subaccountZerosWithoutUsername) { + for (let i = 0; i < config.ATTEMPT_PER_SUBACCOUNT; i++) { + const username: string = generateUsernameForSubaccount( + subaccount.subaccountId, + // Always use subaccountNum 0 for generation. Effectively we are + // generating one username per address. The fact that we are storing + // in the `subaccount_usernames` table is a tech debt. + 0, + // generation nonce + i, + ); + try { + await SubaccountUsernamesTable.create({ username, - error: e, + subaccountId: subaccount.subaccountId, }); + // If success, break from loop and move to next subaccount. + successCount += 1; + break; + } catch (e) { + // There are roughly ~225 million possible usernames + // so the chance of collision is very low. + if (e instanceof Error && e.name === 'UniqueViolationError') { + stats.increment( + `${config.SERVICE_NAME}.subaccount-username-generator.collision`, 1); + logger.info({ + at: 'subaccount-username-generator#runTask', + message: 'username collision', + address: subaccount.address, + subaccountId: subaccount.subaccountId, + username, + error: e, + }); + } else { + logger.error({ + at: 'subaccount-username-generator#runTask', + message: 'Failed to insert username for subaccount', + address: subaccount.address, + subaccountId: subaccount.subaccountId, + username, + error: e, + }); + } } } } + const subaccountAddresses = _.map( + subaccountZerosWithoutUsername, + (subaccount) => subaccount.address, + ); + + logger.info({ + at: 'subaccount-username-generator#runTask', + message: 'Generated usernames', + batchSize: subaccountZerosWithoutUsername.length, + successCount, + addressSample: subaccountAddresses.slice(0, 10), + }); }