From 22ad2bea268aee04701add6f8c2af0c35bf1e42c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 1 Aug 2024 13:57:24 +1000 Subject: [PATCH] Revert "Revert "Faucet referral program"" --- apps/staking/app/faucet/AuthModule.tsx | 20 +- .../app/faucet/[referralCode]/page.tsx | 12 + apps/staking/app/faucet/actions.ts | 115 +++++-- apps/staking/app/faucet/page.tsx | 6 +- apps/staking/app/faucet/utils.tsx | 310 +++++++++++++++++- apps/staking/lib/constants.ts | 4 + apps/staking/locales/en.json | 9 +- 7 files changed, 435 insertions(+), 41 deletions(-) create mode 100644 apps/staking/app/faucet/[referralCode]/page.tsx diff --git a/apps/staking/app/faucet/AuthModule.tsx b/apps/staking/app/faucet/AuthModule.tsx index 9c5d23b1..9b347286 100644 --- a/apps/staking/app/faucet/AuthModule.tsx +++ b/apps/staking/app/faucet/AuthModule.tsx @@ -57,12 +57,13 @@ export const getFaucetFormSchema = () => { }), discordId: z.string().optional(), telegramId: z.string().optional(), + referralCode: z.string().optional(), }); }; export type FaucetFormSchema = z.infer>; -export const AuthModule = () => { +export const AuthModule = ({ referralCode }: { referralCode?: string }) => { const dictionary = useTranslations('faucet.form'); const generalDictionary = useTranslations('general'); const [submitAttemptCounter, setSubmitAttemptCounter] = useState(0); @@ -88,6 +89,7 @@ export const AuthModule = () => { walletAddress: '', discordId: '', telegramId: '', + referralCode, }, reValidateMode: 'onChange', }); @@ -196,6 +198,12 @@ export const AuthModule = () => { } }, [address, ethAmount, form]); */ + useEffect(() => { + if (referralCode) { + toast.info(dictionary('referralCodeAdded')); + } + }, [referralCode]); + useEffect(() => { if (walletStatus === WALLET_STATUS.CONNECTED && address) { form.clearErrors(); @@ -345,10 +353,12 @@ export const AuthModule = () => { <> - {generalDictionary('or')} - - - {!isConnected || (isConnected && discordId) ? : null} - {!isConnected || (isConnected && telegramId) ? : null} - + {!referralCode ? ( + + {!isConnected || (isConnected && discordId) ? : null} + {!isConnected || (isConnected && telegramId) ? : null} + + ) : null} ) : null} diff --git a/apps/staking/app/faucet/[referralCode]/page.tsx b/apps/staking/app/faucet/[referralCode]/page.tsx new file mode 100644 index 00000000..6090e6bb --- /dev/null +++ b/apps/staking/app/faucet/[referralCode]/page.tsx @@ -0,0 +1,12 @@ +import { Faucet } from '@/app/faucet/page'; + +interface FaucetCodePageParams { + params: { + referralCode: string; + }; +} + +export default function FaucetCodePage({ params }: FaucetCodePageParams) { + const { referralCode } = params; + return ; +} diff --git a/apps/staking/app/faucet/actions.ts b/apps/staking/app/faucet/actions.ts index 5a49115b..61e04260 100644 --- a/apps/staking/app/faucet/actions.ts +++ b/apps/staking/app/faucet/actions.ts @@ -17,6 +17,9 @@ import { getLocale, getTranslations } from 'next-intl/server'; import { type Address, formatEther, isAddress as isAddressViem } from 'viem'; import { FaucetFormSchema } from './AuthModule'; import { + codeExists, + getCodeUseTransactionHistory, + getReferralCodeDetails, getTransactionHistory, hasRecentTransaction, idIsInTable, @@ -76,7 +79,6 @@ class FaucetResult { const faucetTokenWarning = BigInt(20000 * Math.pow(10, SENT_DECIMALS)); const faucetGasWarning = BigInt(0.01 * Math.pow(10, ETH_DECIMALS)); -const faucetTokenDrip = BigInt(FAUCET.DRIP * Math.pow(10, SENT_DECIMALS)); const minTargetEthBalance = BigInt(FAUCET.MIN_ETH_BALANCE * Math.pow(10, ETH_DECIMALS)); @@ -138,18 +140,20 @@ export async function transferTestTokens({ walletAddress: targetAddress, discordId, telegramId, + referralCode: code, }: FaucetFormSchema) { const dictionary = await getTranslations('faucet.form.error'); const locale = await getLocale(); let result: FaucetResult = new FaucetResult({}); let db: BetterSql3.Database | undefined; + let faucetTokenDrip = BigInt(FAUCET.DRIP * Math.pow(10, SENT_DECIMALS)); try { if (!isAddress(targetAddress)) { throw new FaucetError( FAUCET_ERROR.INVALID_ADDRESS, - dictionary('invalidAddress', { example: '0x...' }) + dictionary(FAUCET_ERROR.INVALID_ADDRESS, { example: '0x...' }) ); } @@ -158,7 +162,7 @@ export async function transferTestTokens({ */ const chain = process.env.FAUCET_CHAIN; if (!chain || !isChain(chain) || chain !== CHAIN.TESTNET) { - throw new FaucetError(FAUCET_ERROR.INCORRECT_CHAIN, dictionary('incorrectChain')); + throw new FaucetError(FAUCET_ERROR.INCORRECT_CHAIN, dictionary(FAUCET_ERROR.INCORRECT_CHAIN)); } const { faucetAddress, faucetWallet } = await connectFaucetWallet(); @@ -200,21 +204,73 @@ export async function transferTestTokens({ db = openDatabase(); let usedOperatorAddress = false; + let usedWalletListAddress = false; + let usedCode = false; /** - * If the user has not provided a Discord or Telegram ID, they must be an operator. + * If the user provided a referral code, check only the referral code to determine eligibility */ - if (!discordId && !telegramId) { + if (code) { + if (!codeExists({ db, code })) { + throw new FaucetError( + FAUCET_ERROR.INVALID_REFERRAL_CODE, + dictionary(FAUCET_ERROR.INVALID_REFERRAL_CODE) + ); + } + + const { wallet, maxuses, drip: codeDrip } = getReferralCodeDetails({ db, code }); + + if (wallet === targetAddress) { + throw new FaucetError( + FAUCET_ERROR.REFERRAL_CODE_CANT_BE_USED_BY_CREATOR, + dictionary(FAUCET_ERROR.REFERRAL_CODE_CANT_BE_USED_BY_CREATOR) + ); + } + + const codeTransactionHistory = getCodeUseTransactionHistory({ db, code }); + + if (codeTransactionHistory.length >= (maxuses ?? 1)) { + throw new FaucetError( + FAUCET_ERROR.REFERRAL_CODE_OUT_OF_USES, + dictionary(FAUCET_ERROR.REFERRAL_CODE_OUT_OF_USES) + ); + } + if ( - !idIsInTable({ - db, - source: TABLE.OPERATOR, - id: targetAddress, - }) + codeTransactionHistory.filter((transaction) => transaction.target === targetAddress) + .length >= 1 ) { + throw new FaucetError( + FAUCET_ERROR.REFERRAL_CODE_ALREADY_USED, + dictionary(FAUCET_ERROR.REFERRAL_CODE_ALREADY_USED) + ); + } + + if (codeDrip) { + faucetTokenDrip = BigInt(parseInt(codeDrip) * Math.pow(10, SENT_DECIMALS)); + } + + usedCode = true; + } else if (!discordId && !telegramId) { + /** + * If the user has not provided a Discord or Telegram ID, they must be an operator. + */ + const idIsOxenOperator = idIsInTable({ + db, + source: TABLE.OPERATOR, + id: targetAddress, + }); + + const idIsInWalletList = idIsInTable({ + db, + source: TABLE.WALLET, + id: targetAddress, + }); + + if (!idIsOxenOperator && !idIsInWalletList) { throw new FaucetError( FAUCET_ERROR.INVALID_OXEN_ADDRESS, - dictionary('invalidOxenAddress', { + dictionary(FAUCET_ERROR.INVALID_OXEN_ADDRESS, { oxenRegistrationDate: new Intl.DateTimeFormat(locale, { dateStyle: 'long', }).format(new Date(COMMUNITY_DATE.OXEN_SERVICE_NODE_BONUS_PROGRAM)), @@ -223,22 +279,31 @@ export async function transferTestTokens({ } if ( - hasRecentTransaction({ - db, - source: TABLE.OPERATOR, - id: targetAddress, - hoursBetweenTransactions, - }) + (idIsOxenOperator && + hasRecentTransaction({ + db, + source: TABLE.OPERATOR, + id: targetAddress, + hoursBetweenTransactions, + })) || + (idIsInWalletList && + hasRecentTransaction({ + db, + source: TABLE.WALLET, + id: targetAddress, + hoursBetweenTransactions, + })) ) { const transactionHistory = getTransactionHistory({ db, address: targetAddress }); throw new FaucetError( FAUCET_ERROR.ALREADY_USED, - dictionary('alreadyUsed'), + dictionary(FAUCET_ERROR.ALREADY_USED), transactionHistory ); } - usedOperatorAddress = true; + if (idIsOxenOperator) usedOperatorAddress = true; + else if (idIsInWalletList) usedWalletListAddress = true; /** * If the user has provided a Discord ID they must be in the approved list of Discord IDs and not have used the faucet recently. @@ -253,7 +318,7 @@ export async function transferTestTokens({ ) { throw new FaucetError( FAUCET_ERROR.INVALID_SERVICE, - dictionary('invalidService', { + dictionary(FAUCET_ERROR.INVALID_SERVICE, { service: 'Discord', snapshotDate: new Intl.DateTimeFormat(locale, { dateStyle: 'long', @@ -267,7 +332,7 @@ export async function transferTestTokens({ ) { throw new FaucetError( FAUCET_ERROR.ALREADY_USED_SERVICE, - dictionary('alreadyUsedService', { + dictionary(FAUCET_ERROR.ALREADY_USED_SERVICE, { service: 'Discord', }) ); @@ -286,7 +351,7 @@ export async function transferTestTokens({ ) { throw new FaucetError( FAUCET_ERROR.INVALID_SERVICE, - dictionary('invalidService', { + dictionary(FAUCET_ERROR.INVALID_SERVICE, { service: 'Telegram', snapshotDate: new Intl.DateTimeFormat(locale, { dateStyle: 'long', @@ -305,7 +370,7 @@ export async function transferTestTokens({ ) { throw new FaucetError( FAUCET_ERROR.ALREADY_USED_SERVICE, - dictionary('alreadyUsedService', { + dictionary(FAUCET_ERROR.ALREADY_USED_SERVICE, { service: 'Telegram', }) ); @@ -338,7 +403,7 @@ export async function transferTestTokens({ const timestamp = Date.now(); const writeTransactionResult = db .prepare( - `INSERT INTO ${TABLE.TRANSACTIONS} (hash, target, amount, timestamp, discord, telegram, operator, ethhash, ethamount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + `INSERT INTO ${TABLE.TRANSACTIONS} (hash, target, amount, timestamp, discord, telegram, operator, wallet, code, ethhash, ethamount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .run( sessionTokenTxHash, @@ -348,6 +413,8 @@ export async function transferTestTokens({ discordId, telegramId, usedOperatorAddress ? targetAddress : undefined, + usedWalletListAddress ? targetAddress : undefined, + usedCode ? code : undefined, ethTxHash ?? null, ethTopupValue.toString() ); diff --git a/apps/staking/app/faucet/page.tsx b/apps/staking/app/faucet/page.tsx index a7e126ba..19766c0a 100644 --- a/apps/staking/app/faucet/page.tsx +++ b/apps/staking/app/faucet/page.tsx @@ -7,6 +7,10 @@ import { useTranslations } from 'next-intl'; import { AuthModule } from './AuthModule'; export default function FaucetPage() { + return ; +} + +export function Faucet({ referralCode }: { referralCode?: string }) { const dictionary = useTranslations('faucet.information'); return ( @@ -38,7 +42,7 @@ export default function FaucetPage() {

{dictionary.rich('walletRequirementDescription')}

- +
diff --git a/apps/staking/app/faucet/utils.tsx b/apps/staking/app/faucet/utils.tsx index 08353a4d..8274e478 100644 --- a/apps/staking/app/faucet/utils.tsx +++ b/apps/staking/app/faucet/utils.tsx @@ -5,6 +5,8 @@ import { Address } from 'viem'; export enum TABLE { TRANSACTIONS = 'transactions', + CODE = 'code', + WALLET = 'wallet', OPERATOR = 'operator', DISCORD = 'discord', TELEGRAM = 'telegram', @@ -23,6 +25,8 @@ export enum TRANSACTIONS_TABLE { DISCORD = 'discord', TELEGRAM = 'telegram', OPERATOR = 'operator', + WALLET = 'wallet', + CODE = 'code', ETHHASH = 'ethhash', ETHAMOUNT = 'ethamount', } @@ -39,16 +43,16 @@ export enum TELEGRAM_TABLE { ID = 'id', } -export interface OperatorRow { - id: string; -} - -export interface DiscordRow { - id: string; +export enum WALLET_TABLE { + ID = 'id', + NOTE = 'note', } -export interface TelegramRow { - id: string; +export enum CODE_TABLE { + ID = 'id', + WALLET = 'wallet', + DRIP = 'drip', + MAXUSES = 'maxuses', } export interface TransactionsRow { @@ -59,6 +63,10 @@ export interface TransactionsRow { discord?: string; telegram?: string; operator?: string; + wallet?: string; + code?: string; + ethhash?: string; + ethamount?: string; } export const openDatabase = (fileMustExist = true): BetterSql3.Database => { @@ -127,6 +135,22 @@ export const setupDatababse = () => { )` ).run(); + db.prepare( + `CREATE TABLE IF NOT EXISTS ${TABLE.WALLET} ( + ${WALLET_TABLE.ID} TEXT PRIMARY KEY, + ${WALLET_TABLE.NOTE} TEXT + )` + ).run(); + + db.prepare( + `CREATE TABLE IF NOT EXISTS ${TABLE.CODE} ( + ${CODE_TABLE.ID} TEXT PRIMARY KEY, + ${CODE_TABLE.WALLET} TEXT, + ${CODE_TABLE.DRIP} TEXT, + ${CODE_TABLE.MAXUSES} INTEGER + )` + ).run(); + db.prepare( `CREATE TABLE IF NOT EXISTS ${TABLE.TRANSACTIONS} ( ${TRANSACTIONS_TABLE.HASH} TEXT NOT NULL PRIMARY KEY, @@ -136,6 +160,8 @@ export const setupDatababse = () => { ${TRANSACTIONS_TABLE.DISCORD} TEXT, ${TRANSACTIONS_TABLE.TELEGRAM} TEXT, ${TRANSACTIONS_TABLE.OPERATOR} TEXT, + ${TRANSACTIONS_TABLE.WALLET} TEXT, + ${TRANSACTIONS_TABLE.CODE} TEXT, ${TRANSACTIONS_TABLE.ETHHASH} TEXT, ${TRANSACTIONS_TABLE.ETHAMOUNT} TEXT );` @@ -147,6 +173,10 @@ export const setupDatababse = () => { db.prepare( `ALTER TABLE ${TABLE.TRANSACTIONS} ADD COLUMN ${TRANSACTIONS_TABLE.ETHAMOUNT} TEXT;` ).run(); */ + // db.prepare( + // `ALTER TABLE ${TABLE.TRANSACTIONS} ADD COLUMN ${TRANSACTIONS_TABLE.WALLET} TEXT;` + // ).run(); + // db.prepare(`ALTER TABLE ${TABLE.TRANSACTIONS} ADD COLUMN ${TRANSACTIONS_TABLE.CODE} TEXT;`).run(); db.prepare( `CREATE INDEX IF NOT EXISTS discord_index ON ${TABLE.TRANSACTIONS} (${TRANSACTIONS_TABLE.DISCORD}, ${TRANSACTIONS_TABLE.TIMESTAMP}) WHERE ${TRANSACTIONS_TABLE.DISCORD} IS NOT NULL;` @@ -160,6 +190,14 @@ export const setupDatababse = () => { `CREATE INDEX IF NOT EXISTS operator_index ON ${TABLE.TRANSACTIONS} (${TRANSACTIONS_TABLE.OPERATOR}, ${TRANSACTIONS_TABLE.TIMESTAMP}) WHERE ${TRANSACTIONS_TABLE.OPERATOR} IS NOT NULL;` ).run(); + db.prepare( + `CREATE INDEX IF NOT EXISTS operator_index ON ${TABLE.TRANSACTIONS} (${TRANSACTIONS_TABLE.WALLET}, ${TRANSACTIONS_TABLE.TIMESTAMP}) WHERE ${TRANSACTIONS_TABLE.WALLET} IS NOT NULL;` + ).run(); + + db.prepare( + `CREATE INDEX IF NOT EXISTS operator_index ON ${TABLE.TRANSACTIONS} (${TRANSACTIONS_TABLE.TARGET}, ${TRANSACTIONS_TABLE.TIMESTAMP}, ${TRANSACTIONS_TABLE.CODE}) WHERE ${TRANSACTIONS_TABLE.CODE} IS NOT NULL;` + ).run(); + db.close(); }; @@ -300,7 +338,7 @@ export const hasCount = (row: CountType, countField: F) => type IdTableParams = { db: BetterSql3.Database; - source: TABLE.DISCORD | TABLE.TELEGRAM | TABLE.OPERATOR; + source: TABLE.DISCORD | TABLE.TELEGRAM | TABLE.OPERATOR | TABLE.WALLET | TABLE.CODE; id: string; }; @@ -370,6 +408,260 @@ export function getTransactionHistory({ .all(address) as Array; } +type CodeExistsParams = { + db: BetterSql3.Database; + code: string; +}; + +/** + * Checks if the given referral code exists. + * @param props Code exists props. + * @param props.db The database instance. + * @param props.code The code to check for existence. + * @returns A boolean indicating whether the code exists in the table. + */ +export function codeExists({ db, code }: CodeExistsParams) { + return idIsInTable({ db, source: TABLE.CODE, id: code }); +} + +type CodeDetailParams = CodeExistsParams; + +type CodeDetails = { + id: string; + wallet?: string; + drip?: string; + maxuses?: number; +}; + +/** + * Get a referral codes details. + * @param props Get referral code details props. + * @param props.db The database instance. + * @param props.code The code to get details for. + * @returns The referral code details. + */ +export function getReferralCodeDetails({ db, code }: CodeDetailParams): CodeDetails { + return db + .prepare( + `SELECT ${CODE_TABLE.ID}, ${CODE_TABLE.WALLET}, ${CODE_TABLE.DRIP}, ${CODE_TABLE.MAXUSES} FROM ${TABLE.CODE} WHERE ${CODE_TABLE.ID} = ?` + ) + .all(code)[0] as CodeDetails; +} + +type CodeUseTransactionHistory = TransactionHistory & { target: Address }; + +/** + * Get a referral code's transaction history. This is all the times a code was used. + * @param props Get code transaction history props. + * @param props.db The database instance. + * @param props.code The code to get history for. + * @returns A list of transactions for a referral code. + */ +export function getCodeUseTransactionHistory({ + db, + code, +}: { + db: BetterSql3.Database; + code: string; +}): Array { + return db + .prepare( + `SELECT ${TRANSACTIONS_TABLE.TIMESTAMP}, ${TRANSACTIONS_TABLE.HASH}, ${TRANSACTIONS_TABLE.AMOUNT}, ${TRANSACTIONS_TABLE.ETHHASH}, ${TRANSACTIONS_TABLE.ETHAMOUNT}, ${TRANSACTIONS_TABLE.TARGET} FROM ${TABLE.TRANSACTIONS} WHERE ${TRANSACTIONS_TABLE.CODE} = ? ORDER BY ${TRANSACTIONS_TABLE.TIMESTAMP} DESC` + ) + .all(code) as Array; +} + +/** + * Determines if the parameters for generating a number of codes are sufficient for generating + * the requested number of codes given the available character set and code length. + * @param numCodes The number of codes to generate. + * @param codeLength The length of the codes. + * @param characters The character set to use to create the codes. + * @returns If the parameters for generating a number of codes is sufficient + */ +function canCreateRequestedCodes(numCodes: number, codeLength: number, characters: string) { + const numUniqueCombinations = Math.pow(characters.length, codeLength); + return numUniqueCombinations >= numCodes; +} + +const charsets = { + alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + safeAlphanumeric: 'ABCDEFGHJKLMNPRSTUVXYZ0123456789', + custom: '', +} as const; + +type GenerateReferralCodesParams = { + codeLength: number; + charset: keyof typeof charsets; + numberOfCodes: number; + prefix?: string; + suffix?: string; + customCharset?: string; + existingCodes: Array; +}; + +/** + * Generate random unique referral codes. + * @param props Referral code generator props + * @param props.codeLength The length of the codes. + * @param props.charset The characters to use in creating the codes. + * @param props.numberOfCodes The number of codes to create. + * @param props.prefix The prefix to use at the start of the code. NOTE: this reduces the number of random characters in the code. + * @param props.suffix The suffix to use at the end of the code. NOTE: this reduces the number of random characters in the code. + * @param props.customCharset A custom charset to use if {@link charset} is set to {@link charsets.custom}. + * @param props.existingCodes A list of existing referral codes to prevent duplication. + * @returns The generated referral codes. + */ +function generateRandomReferralCodes({ + codeLength, + charset, + numberOfCodes, + prefix, + suffix, + customCharset, + existingCodes, +}: GenerateReferralCodesParams) { + const codes = new Set(); + const codeRandomLength = codeLength - (prefix?.length ?? 0) - (suffix?.length ?? 0); + + const validChars = charset === 'custom' && customCharset ? customCharset : charsets[charset]; + + if (!canCreateRequestedCodes(numberOfCodes, codeRandomLength, validChars)) { + throw new Error( + 'Unable to generate the requested codes. Too many number of codes, too short length, or too short charset.' + ); + } + + /** + * 1. Generate random codes until to the target is reached (internal do-while loop). + * 2. Remove non-unique codes. + * 3. If more codes are needed to reach the target, repeat. + */ + do { + /** + * Keeps generating random codes until the number of codes in the set (unique) is equal to the + * goal numberOfCodes. + */ + do { + const code = `${prefix ?? ''}${generateRandomReferralCode(validChars, codeRandomLength)}${suffix ?? ''}`; + codes.add(code); + } while (codes.size < numberOfCodes); + + /** + * Remove any codes from the set that already exist + */ + existingCodes.forEach((str) => { + if (codes.has(str)) { + codes.delete(str); + } + }); + + /** + * If any codes have been removed due to a uniqueness collision, keep generating + * the codes up to the number of codes again and repeat until all the codes are generated and + * unique + */ + } while (codes.size < numberOfCodes); + + return codes; +} + +/** + * Generate a random referral code. + * @param charset The character set to generate the code from. + * @param codeLength The length of the coed to generate. + * @returns A randomly generated code. + */ +function generateRandomReferralCode(charset: string, codeLength: number) { + let code = ''; + for (let i = 0; i < codeLength; i++) { + const randomIndex = Math.floor(Math.random() * charset.length); + code += charset[randomIndex]; + } + return code; +} + +type AddReferralCodesParams = { + codes: Set; + creatorWallet?: string; + maxUses?: number; + drip?: bigint; +}; + +/** + * Get existing referral codes from the database + * @param db The database instance + */ +function getExistingReferralCodes(db: BetterSql3.Database) { + const rows = db.prepare(`SELECT ${CODE_TABLE.ID} FROM ${TABLE.CODE}`).all() as Array<{ + id: string; + }>; + return rows.flatMap((row) => row.id); +} + +/** + * Add referral codes to the database + * @param props Referral code props + * @param props.codes The codes to add + * @param props.maxUses The maximum number of uses the codes can have + * @param props.drip The amount of tokens the code will set to be released + * @param props.creatorWallet The wallet the code was created with (if it was created for a wallet) + */ +function addReferralCodes({ codes, maxUses, drip, creatorWallet }: AddReferralCodesParams) { + let db: BetterSql3.Database | undefined; + + try { + const db = openDatabase(); + + const insert = db.prepare( + `INSERT INTO ${TABLE.CODE} (${CODE_TABLE.ID}, ${CODE_TABLE.WALLET}, ${CODE_TABLE.DRIP}, ${CODE_TABLE.MAXUSES}) VALUES (?, ?, ?, ?)` + ); + + const transaction = db.transaction((newCodes: Set) => { + for (const code of newCodes) { + insert.run(code, creatorWallet, drip?.toString(), maxUses); + } + }); + + transaction(codes); + } catch (error) { + console.error('Failed to add referral codes', error); + } finally { + if (db) { + db.close(); + } + } +} + +type CreateReferralCodesParams = { + db: BetterSql3.Database; + generateParams: Omit; + addParams: Omit; +}; + +/** + * Create referral codes + * @param props Create referral code props + * @param props.db The database instance + * @param props.generateParams params for {@link generateRandomReferralCodes} + * @param props.addParams params for {@link getExistingReferralCodes} + */ +export function createReferralCodes({ + db, + generateParams, + addParams, +}: CreateReferralCodesParams): Set { + try { + const existingCodes = getExistingReferralCodes(db); + const codes = generateRandomReferralCodes({ ...generateParams, existingCodes }); + addReferralCodes({ ...addParams, codes }); + return codes; + } catch (error) { + console.error('Failed to creat referral codes', error); + return new Set(); + } +} + /* interface NodeOperatorScoreResponse { global_score: number; diff --git a/apps/staking/lib/constants.ts b/apps/staking/lib/constants.ts index e82a980e..8ee5a942 100644 --- a/apps/staking/lib/constants.ts +++ b/apps/staking/lib/constants.ts @@ -61,6 +61,10 @@ export enum FAUCET_ERROR { INVALID_OXEN_ADDRESS = 'invalidOxenAddress', ALREADY_USED = 'alreadyUsed', ALREADY_USED_SERVICE = 'alreadyUsedService', + INVALID_REFERRAL_CODE = 'invalidReferralCode', + REFERRAL_CODE_CANT_BE_USED_BY_CREATOR = 'referralCodeCantBeUsedByCreator', + REFERRAL_CODE_OUT_OF_USES = 'referralCodeOutOfUses', + REFERRAL_CODE_ALREADY_USED = 'referralCodeAlreadyUsed', } export enum TICKER { diff --git a/apps/staking/locales/en.json b/apps/staking/locales/en.json index 994a339a..36113972 100644 --- a/apps/staking/locales/en.json +++ b/apps/staking/locales/en.json @@ -141,14 +141,19 @@ "invalidOxenAddress": "This wallet address was not registered for the {oxenProgram} on {oxenRegistrationDate} and is not eligible for testnet access. Try connecting with Discord or Telegram instead.", "alreadyUsedService": "The connected {service} account has already been used to claim testnet {tokenSymbol} for another wallet address.", "alreadyUsed": "The connected wallet address has already been used to claim testnet {tokenSymbol}.", - "authFailed": "{service} authentication failed. Please refresh the page and try again." + "authFailed": "{service} authentication failed. Please refresh the page and try again.", + "invalidReferralCode": "Referral code is invalid.", + "referralCodeCantBeUsedByCreator": "Referral code can't be used by this wallet, consider sharing it with someone.", + "referralCodeOutOfUses": "Referral code is out of uses.", + "referralCodeAlreadyUsed": "You have already used this referral code." }, "transactionHash": "Transaction Hash: {hash}", "transactionHashDescription": "Transaction hash of the faucet transaction.", "haveSomeMore": "Alright then, you can have some more", "watchSENTInfo": "You may need to manually add {tokenSymbol} to your wallet’s token list to make it show up, click the button below to add it.", "copyTransactionHash": "Copy Transaction Hash", - "copyTransactionHashSuccessToast": "Copied Transaction Hash to clipboard!" + "copyTransactionHashSuccessToast": "Copied Transaction Hash to clipboard!", + "referralCodeAdded": "Faucet referral code detected and added." } }, "chainBanner": {