From b18052216d8e90bf6bf8190fd212af9414797b25 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 7 Nov 2024 15:06:24 +1100 Subject: [PATCH 1/4] feat: referral program --- apps/staking/.env.local.template | 3 +- apps/staking/app/faucet/AuthModule.tsx | 20 +- apps/staking/app/faucet/Faucet.tsx | 44 +++ apps/staking/app/faucet/[code]/page.tsx | 14 + apps/staking/app/faucet/actions.ts | 157 ++++++-- apps/staking/app/faucet/page.tsx | 44 +-- apps/staking/app/faucet/utils.tsx | 348 +++++++++++++++++- .../app/mystakes/modules/ComingSoon.tsx | 15 + .../app/mystakes/modules/ReferralModule.tsx | 83 +++++ .../mystakes/modules/TestnetPointsModule.tsx | 88 +++++ apps/staking/app/mystakes/page.tsx | 14 +- apps/staking/lib/constants.ts | 6 + apps/staking/lib/hashid.ts | 33 ++ apps/staking/locales/en.json | 24 +- apps/staking/package.json | 5 +- apps/staking/testing/data-test-ids.tsx | 2 + pnpm-lock.yaml | 27 +- turbo.json | 3 +- 18 files changed, 814 insertions(+), 116 deletions(-) create mode 100644 apps/staking/app/faucet/Faucet.tsx create mode 100644 apps/staking/app/faucet/[code]/page.tsx create mode 100644 apps/staking/app/mystakes/modules/ComingSoon.tsx create mode 100644 apps/staking/app/mystakes/modules/ReferralModule.tsx create mode 100644 apps/staking/app/mystakes/modules/TestnetPointsModule.tsx create mode 100644 apps/staking/lib/hashid.ts diff --git a/apps/staking/.env.local.template b/apps/staking/.env.local.template index 3f23b97a..c29fac47 100644 --- a/apps/staking/.env.local.template +++ b/apps/staking/.env.local.template @@ -10,4 +10,5 @@ DISCORD_CLIENT_ID= DISCORD_CLIENT_SECRET= TELEGRAM_BOT_TOKEN= NEXTAUTH_SECRET= -NEXTAUTH_URL= \ No newline at end of file +NEXTAUTH_URL= +NEXT_PUBLIC_POINTS_PROGRAM_API= \ No newline at end of file diff --git a/apps/staking/app/faucet/AuthModule.tsx b/apps/staking/app/faucet/AuthModule.tsx index 72cc3ee7..8b39770d 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(), + code: z.string().optional(), }); }; export type FaucetFormSchema = z.infer>; -export const AuthModule = () => { +export const AuthModule = ({ code }: { code?: 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: '', + code: code ?? '', }, reValidateMode: 'onChange', }); @@ -196,6 +198,12 @@ export const AuthModule = () => { } }, [address, ethAmount, form]); */ + useEffect(() => { + if (code) { + toast.info(dictionary('referralCodeAdded')); + } + }, [code]); + 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} - + {!code ? ( + + {!isConnected || (isConnected && discordId) ? : null} + {!isConnected || (isConnected && telegramId) ? : null} + + ) : null} ) : null} diff --git a/apps/staking/app/faucet/Faucet.tsx b/apps/staking/app/faucet/Faucet.tsx new file mode 100644 index 00000000..b1681988 --- /dev/null +++ b/apps/staking/app/faucet/Faucet.tsx @@ -0,0 +1,44 @@ +import { useTranslations } from 'next-intl'; +import { NextAuthProvider } from '@session/auth/client'; +import { formatDate, formatList } from '@/lib/locale-client'; +import { COMMUNITY_DATE } from '@/lib/constants'; +import { AuthModule } from '@/app/faucet/AuthModule'; + +export function Faucet({ code }: { code?: string }) { + const dictionary = useTranslations('faucet.information'); + return ( + +
+
+

{dictionary('title')}

+

{dictionary('communityTitle')}

+

+ {dictionary.rich('communityDescription', { + connectionOptions: formatList(['Discord', 'Telegram']), + snapshotDate: formatDate(new Date(COMMUNITY_DATE.SESSION_TOKEN_COMMUNITY_SNAPSHOT), { + dateStyle: 'long', + }), + })} +

+

{dictionary('oxenTitle')}

+

+ {dictionary.rich('oxenDescription', { + oxenRegistrationDate: formatDate( + new Date(COMMUNITY_DATE.OXEN_SERVICE_NODE_BONUS_PROGRAM), + { + dateStyle: 'long', + } + ), + })} +

+

{dictionary('notEligible')}

+

{dictionary('walletRequirementTitle')}

+

{dictionary.rich('walletRequirementDescription')}

+
+
+ +
+
+
+ ); +} diff --git a/apps/staking/app/faucet/[code]/page.tsx b/apps/staking/app/faucet/[code]/page.tsx new file mode 100644 index 00000000..e8cf3ed3 --- /dev/null +++ b/apps/staking/app/faucet/[code]/page.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { Faucet } from '@/app/faucet/Faucet'; + +interface FaucetCodePageParams { + params: { + code: string; + }; +} + +export default function FaucetCodePage({ params }: FaucetCodePageParams) { + const { code } = params; + return ; +} diff --git a/apps/staking/app/faucet/actions.ts b/apps/staking/app/faucet/actions.ts index f79452bf..57b87e43 100644 --- a/apps/staking/app/faucet/actions.ts +++ b/apps/staking/app/faucet/actions.ts @@ -1,7 +1,14 @@ 'use server'; import { COMMUNITY_DATE, FAUCET, FAUCET_ERROR, TICKER } from '@/lib/constants'; -import { addresses, CHAIN, chains, SENT_DECIMALS, SENT_SYMBOL } from '@session/contracts'; +import { + addresses, + CHAIN, + formatSENT, + isChain, + SENT_DECIMALS, + SENT_SYMBOL, +} from '@session/contracts'; import { SENTAbi } from '@session/contracts/abis'; import { ETH_DECIMALS } from '@session/wallet/lib/eth'; import { createPublicWalletClient, createServerWallet } from '@session/wallet/lib/server-wallet'; @@ -10,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, @@ -18,7 +28,6 @@ import { TABLE, TransactionHistory, } from './utils'; -import { formatSENTBigInt } from '@session/contracts/hooks/SENT'; class FaucetError extends Error { faucetError: FAUCET_ERROR; @@ -70,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)); @@ -132,24 +140,32 @@ export async function transferTestTokens({ walletAddress: targetAddress, discordId, telegramId, + 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...' }) ); } + /** + * NOTE: This enforces a set chain but its locked to {@see CHAIN.TESTNET} for now this should be changed to allow for multiple chains. + */ + const chain = process.env.FAUCET_CHAIN; + if (!chain || !isChain(chain) || chain !== CHAIN.TESTNET) { + throw new FaucetError(FAUCET_ERROR.INCORRECT_CHAIN, dictionary(FAUCET_ERROR.INCORRECT_CHAIN)); + } + const { faucetAddress, faucetWallet } = await connectFaucetWallet(); - const chain = CHAIN.TESTNET; const [targetEthBalance, faucetEthBalance, faucetTokenBalance] = await Promise.all([ getEthBalance({ address: targetAddress, chain }), getEthBalance({ address: faucetAddress, chain }), @@ -180,28 +196,77 @@ export async function transferTestTokens({ */ if (faucetTokenBalance < faucetTokenWarning) { console.warn( - `Faucet wallet ${SENT_SYMBOL} balance (${formatSENTBigInt(faucetTokenBalance)}) is below the warning threshold (${formatSENTBigInt(faucetTokenWarning)})` + `Faucet wallet ${SENT_SYMBOL} balance (${formatSENT(faucetTokenBalance)} ${SENT_SYMBOL}) is below the warning threshold (${formatSENT(faucetTokenWarning)})` ); } 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 ( - !idIsInTable({ - db, - source: TABLE.OPERATOR, - id: targetAddress, - }) - ) { + if (code) { + if (!codeExists({ db, code })) { + throw new FaucetError( + FAUCET_ERROR.INVALID_REFERRAL_CODE, + dictionary(FAUCET_ERROR.INVALID_REFERRAL_CODE) + ); + } + + const { wallet, maxuses: 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 (codeTransactionHistory.some((transaction) => transaction.target === targetAddress)) { + throw new FaucetError( + FAUCET_ERROR.REFERRAL_CODE_ALREADY_USED, + dictionary(FAUCET_ERROR.REFERRAL_CODE_ALREADY_USED) + ); + } + + if (codeDrip) { + faucetTokenDrip = BigInt(codeDrip); + } + + 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)), @@ -210,22 +275,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. @@ -240,7 +314,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', @@ -254,7 +328,7 @@ export async function transferTestTokens({ ) { throw new FaucetError( FAUCET_ERROR.ALREADY_USED_SERVICE, - dictionary('alreadyUsedService', { + dictionary(FAUCET_ERROR.ALREADY_USED_SERVICE, { service: 'Discord', }) ); @@ -273,7 +347,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', @@ -292,7 +366,7 @@ export async function transferTestTokens({ ) { throw new FaucetError( FAUCET_ERROR.ALREADY_USED_SERVICE, - dictionary('alreadyUsedService', { + dictionary(FAUCET_ERROR.ALREADY_USED_SERVICE, { service: 'Telegram', }) ); @@ -312,21 +386,22 @@ export async function transferTestTokens({ */ let ethTxHash: Address | undefined; const ethTopupValue = minTargetEthBalance - targetEthBalance; - if (ethTopupValue > 0) { - const request = await faucetWallet.prepareTransactionRequest({ - to: targetAddress, - value: ethTopupValue, - chain: chains[chain], - }); - - const serializedTransaction = await faucetWallet.signTransaction(request); - ethTxHash = await faucetWallet.sendRawTransaction({ serializedTransaction }); - } + // TODO: fix this + // if (ethTopupValue > 0) { + // const request = await faucetWallet.prepareTransactionRequest({ + // to: targetAddress, + // value: ethTopupValue, + // chain: chains[chain], + // }); + // + // const serializedTransaction = await faucetWallet.signTransaction(request); + // ethTxHash = await faucetWallet.sendRawTransaction({ serializedTransaction }); + // } 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, @@ -336,6 +411,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..01313de4 100644 --- a/apps/staking/app/faucet/page.tsx +++ b/apps/staking/app/faucet/page.tsx @@ -1,46 +1,6 @@ 'use client'; -import { NextAuthProvider } from '@session/auth/client'; - -import { COMMUNITY_DATE } from '@/lib/constants'; -import { formatDate, formatList } from '@/lib/locale-client'; -import { useTranslations } from 'next-intl'; -import { AuthModule } from './AuthModule'; +import { Faucet } from '@/app/faucet/Faucet'; export default function FaucetPage() { - const dictionary = useTranslations('faucet.information'); - return ( - -
-
-

{dictionary('title')}

-

{dictionary('communityTitle')}

-

- {dictionary.rich('communityDescription', { - connectionOptions: formatList(['Discord', 'Telegram']), - snapshotDate: formatDate(new Date(COMMUNITY_DATE.SESSION_TOKEN_COMMUNITY_SNAPSHOT), { - dateStyle: 'long', - }), - })} -

-

{dictionary('oxenTitle')}

-

- {dictionary.rich('oxenDescription', { - oxenRegistrationDate: formatDate( - new Date(COMMUNITY_DATE.OXEN_SERVICE_NODE_BONUS_PROGRAM), - { - dateStyle: 'long', - } - ), - })} -

-

{dictionary('notEligible')}

-

{dictionary('walletRequirementTitle')}

-

{dictionary.rich('walletRequirementDescription')}

-
-
- -
-
-
- ); + return ; } diff --git a/apps/staking/app/faucet/utils.tsx b/apps/staking/app/faucet/utils.tsx index ff3cb89a..27561bf8 100644 --- a/apps/staking/app/faucet/utils.tsx +++ b/apps/staking/app/faucet/utils.tsx @@ -1,10 +1,13 @@ -import { isProduction } from '@session/util-js/env'; import Database, * as BetterSql3 from 'better-sqlite3-multiple-ciphers'; import path from 'path'; import { Address } from 'viem'; +import { isProduction } from '@/lib/env'; +import { encodeAddressToHashId } from '@/lib/hashid'; export enum TABLE { TRANSACTIONS = 'transactions', + CODE = 'code', + WALLET = 'wallet', OPERATOR = 'operator', DISCORD = 'discord', TELEGRAM = 'telegram', @@ -23,6 +26,8 @@ export enum TRANSACTIONS_TABLE { DISCORD = 'discord', TELEGRAM = 'telegram', OPERATOR = 'operator', + WALLET = 'wallet', + CODE = 'code', ETHHASH = 'ethhash', ETHAMOUNT = 'ethamount', } @@ -39,16 +44,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 +64,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 => { @@ -79,7 +88,7 @@ export const openDatabase = (fileMustExist = true): BetterSql3.Database => { fileMustExist, }; - if (!isProduction()) { + if (!isProduction) { dbOptions.verbose = console.log; } @@ -127,6 +136,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 +161,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 +174,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 +191,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_wallet 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_code ON ${TABLE.TRANSACTIONS} (${TRANSACTIONS_TABLE.TARGET}, ${TRANSACTIONS_TABLE.TIMESTAMP}, ${TRANSACTIONS_TABLE.CODE}) WHERE ${TRANSACTIONS_TABLE.CODE} IS NOT NULL;` + ).run(); + db.close(); }; @@ -300,7 +339,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 +409,293 @@ 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(); + } +} + +type CreateWalletReferralCodesParams = { + params: Array; +}; + +export function createWalletReferralCodes({ params }: CreateWalletReferralCodesParams) { + try { + for (const { codes, maxUses, drip, creatorWallet } of params) { + addReferralCodes({ codes, maxUses, drip, creatorWallet }); + } + } catch (error) { + console.error('Failed to creat referral codes', error); + } +} + +const test: Array<{ address: Address; uses: number }> = [ + { address: '0x501bcb8d257b8115dc6c5048bA34A4B9E668d46e', uses: 1 }, + { address: '0xd2A954dE41b04E999d1B94dB56B27AFaF4389053', uses: 1 }, + { address: '0x9EbDB42452eA9c01BeBaA4955ad3b875C461E065', uses: 1 }, + { address: '0x6Be297b19d6dEDDC4Cd61a54A01f054670F8703E', uses: 2 }, + { address: '0x349c2968926fdE3bb357eBD92519cE5Ec35E270A', uses: 3 }, + { address: '0xEaA90e3f8801B9797435C6A026deB1f4A018B83f', uses: 4 }, +]; + +export function createWalletCodes() { + const params: Array = test.map(({ address, uses }) => ({ + codes: new Set([encodeAddressToHashId(address)]), + creatorWallet: address, + maxUses: uses, + drip: 5000000000000n, + })); + createWalletReferralCodes({ params }); +} + /* interface NodeOperatorScoreResponse { global_score: number; diff --git a/apps/staking/app/mystakes/modules/ComingSoon.tsx b/apps/staking/app/mystakes/modules/ComingSoon.tsx new file mode 100644 index 00000000..a9a5a003 --- /dev/null +++ b/apps/staking/app/mystakes/modules/ComingSoon.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { Module, ModuleTitle, ModuleTooltip } from '@session/ui/components/Module'; +import { useTranslations } from 'next-intl'; + +export default function ComingSoonModule() { + const dictionary = useTranslations('modules.comingSoon'); + + return ( + + {dictionary.rich('description')} + {dictionary('title')} + + ); +} diff --git a/apps/staking/app/mystakes/modules/ReferralModule.tsx b/apps/staking/app/mystakes/modules/ReferralModule.tsx new file mode 100644 index 00000000..f38c48af --- /dev/null +++ b/apps/staking/app/mystakes/modules/ReferralModule.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { Module, ModuleContent, ModuleHeader, ModuleText } from '@session/ui/components/Module'; +import { useTranslations } from 'next-intl'; +import { Input } from '@session/ui/ui/input'; +import { useMemo, useRef, useState } from 'react'; +import { useWallet } from '@session/wallet/hooks/wallet-hooks'; +import { encodeAddressToHashId } from '@/lib/hashid'; +import { WalletModalButtonWithLocales } from '@/components/WalletModalButtonWithLocales'; +import { BASE_URL, URL } from '@/lib/constants'; +import { CopyToClipboardButton } from '@session/ui/components/CopyToClipboardButton'; +import { ButtonDataTestId } from '@/testing/data-test-ids'; +import { Button } from '@session/ui/ui/button'; +import { toast } from '@session/ui/lib/toast'; +import { externalLink } from '@/lib/locale-defaults'; + +export default function ReferralModule() { + const [hidden, setHidden] = useState(true); + const inputRef = useRef(null); + const dictionary = useTranslations('modules.referral'); + const clipboardDictionary = useTranslations('clipboard'); + + const { address } = useWallet(); + + const referralLink = useMemo(() => { + if (!address) return null; + const hashId = encodeAddressToHashId(address); + return `${BASE_URL}/faucet/${hashId}`; + }, [address]); + + return ( + + + {dictionary('title')} +

+ {dictionary('description1')} +
+
+ {dictionary.rich('description2', { link: externalLink(URL.TESTNET_REFERRALS) })} +
+
+ {dictionary.rich('description3', { link: externalLink(URL.TESTNET_REFERRALS_TOS) })} +

+
+ + {address ? ( +
+ {!hidden && referralLink ? ( +
+ e.target.select()} + /> + { + inputRef.current?.select(); + toast.success(clipboardDictionary('copyToClipboardSuccessToast')); + }} + /> +
+ ) : ( + + )} +
+ ) : ( + + )} +
+
+ ); +} diff --git a/apps/staking/app/mystakes/modules/TestnetPointsModule.tsx b/apps/staking/app/mystakes/modules/TestnetPointsModule.tsx new file mode 100644 index 00000000..6f44a908 --- /dev/null +++ b/apps/staking/app/mystakes/modules/TestnetPointsModule.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { URL } from '@/lib/constants'; +import { externalLink } from '@/lib/locale-defaults'; +import { Module, ModuleTitle, ModuleTooltip } from '@session/ui/components/Module'; +import { useTranslations } from 'next-intl'; +import { useWallet } from '@session/wallet/hooks/wallet-hooks'; +import { + getVariableFontSizeForSmallModule, + ModuleDynamicQueryText, +} from '@/components/ModuleDynamic'; +import type { QUERY_STATUS } from '@/lib/query'; +import { useMemo } from 'react'; +import { Address } from 'viem'; +import { useQuery } from '@tanstack/react-query'; +import { toast } from '@session/ui/lib/toast'; +import { areHexesEqual } from '@session/util-crypto/string'; +import { formatNumber } from '@/lib/locale-client'; + +const noPointsObject = { + score: 0, + percent: 0, +}; + +export default function TestnetPointsModule(params?: { addressOverride?: Address }) { + const dictionary = useTranslations('modules.points'); + const toastDictionary = useTranslations('modules.toast'); + const titleFormat = useTranslations('modules.title'); + const title = dictionary('title'); + + const { address: connectedAddress } = useWallet(); + const address = useMemo( + () => params?.addressOverride ?? connectedAddress, + [params?.addressOverride, connectedAddress] + ); + + const { data, status, refetch } = useQuery({ + queryKey: ['points', address], + queryFn: async () => { + const res = await fetch(process.env.NEXT_PUBLIC_POINTS_PROGRAM_API!); + + if (!res.ok) { + toast.error('Failed to fetch points'); + } + + const data = await res.json(); + if (!data?.wallets) return noPointsObject; + + const pointsData = Object.entries(data.wallets).find(([wallet]) => + areHexesEqual(wallet, address) + ); + + if (!pointsData || !pointsData[1]) return noPointsObject; + + return pointsData[1] as { score: number; percent: number }; + }, + }); + + const points = data?.score ? `${formatNumber(data.score)} points` : null; + + return ( + + + {dictionary.rich('description', { + link: externalLink(URL.SESSION_TOKEN_COMMUNITY_SNAPSHOT), + })} + + {titleFormat('format', { title })} + + {points} + + + ); +} diff --git a/apps/staking/app/mystakes/page.tsx b/apps/staking/app/mystakes/page.tsx index 3c983956..122158f1 100644 --- a/apps/staking/app/mystakes/page.tsx +++ b/apps/staking/app/mystakes/page.tsx @@ -7,6 +7,11 @@ import PriceModule from './modules/PriceModule'; import StakedNodesModule from './modules/StakedNodesModule'; import TotalRewardsModule from './modules/TotalRewardsModule'; import UnclaimedTokensModule from './modules/UnclaimedTokensModule'; +import { useChain } from '@session/contracts/hooks/useChain'; +import { CHAIN } from '@session/contracts'; +import ReferralModule from '@/app/mystakes/modules/ReferralModule'; +import TestnetPointsModule from '@/app/mystakes/modules/TestnetPointsModule'; +import ComingSoonModule from '@/app/mystakes/modules/ComingSoon'; export async function generateMetadata() { return siteMetadata({ @@ -16,17 +21,20 @@ export async function generateMetadata() { } export default function Page() { + const chain = useChain(); return (
- - + + + + - + {chain === CHAIN.TESTNET ? : }
diff --git a/apps/staking/lib/constants.ts b/apps/staking/lib/constants.ts index 09303bc3..19866079 100644 --- a/apps/staking/lib/constants.ts +++ b/apps/staking/lib/constants.ts @@ -22,6 +22,8 @@ export enum URL { SESSION_TOKEN_COMMUNITY_SNAPSHOT = 'https://token.getsession.org/testnet-incentive-program', INCENTIVE_PROGRAM_TOS = 'https://token.getsession.org/incentive-program-terms', BUG_BOUNTY_PROGRAM = 'https://token.getsession.org/bug-bounty-program', + TESTNET_REFERRALS = 'https://token.getsession.org/blog/testnet-referrals', + TESTNET_REFERRALS_TOS = 'https://token.getsession.org/referral-program-terms', BUG_BOUNTY_TOS = 'https://token.getsession.org/bug-bounty-terms', SESSION_NODE_SOLO_SETUP_DOCS = 'https://docs.getsession.org/class-is-in-session/session-stagenet-single-contributor-node-setup', REMOVE_TOKEN_FROM_WATCH_LIST = 'https://support.metamask.io/managing-my-tokens/custom-tokens/how-to-remove-a-token/', @@ -63,6 +65,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/lib/hashid.ts b/apps/staking/lib/hashid.ts new file mode 100644 index 00000000..c1992d47 --- /dev/null +++ b/apps/staking/lib/hashid.ts @@ -0,0 +1,33 @@ +import Sqids from 'sqids'; +import { type Address, checksumAddress } from 'viem'; + +const sqids = new Sqids(); + +function ethereumAddressToNumberArray(address: string) { + const parsedAddress = address.toLowerCase().startsWith('0x') ? address.slice(2) : address; + + // Step 2: Validate the address length (should be 40 characters for 20 bytes) + if (parsedAddress.length !== 40) { + throw new Error('Invalid Ethereum address length'); + } + + // Step 3 and 4: Split into byte pairs and convert to numbers + const numberArray = []; + for (let i = 0; i < parsedAddress.length; i += 2) { + const byteString = parsedAddress.slice(i, i + 2); + // Parse each hex byte to an integer + const byteValue = parseInt(byteString, 16); + if (isNaN(byteValue)) { + throw new Error(`Invalid hex character detected: ${byteString}`); + } + numberArray.push(byteValue); + } + + return numberArray; +} + +export const encodeAddressToHashId = (address: Address) => { + const formattedAddress = checksumAddress(address); + const int = ethereumAddressToNumberArray(formattedAddress); + return sqids.encode(int); +}; diff --git a/apps/staking/locales/en.json b/apps/staking/locales/en.json index 8924c52a..cc98c3c2 100644 --- a/apps/staking/locales/en.json +++ b/apps/staking/locales/en.json @@ -148,14 +148,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": { @@ -274,6 +279,21 @@ "price": { "title": "Price", "description": "Current price of {tokenSymbol}." + }, + "points": { + "title": "Testnet Points", + "description": "These points determine your eventual share of Session Tokens as part of the Testnet Incentive Program. Learn more." + }, + "comingSoon": { + "title": "Coming Soon", + "description": "Coming Soon" + }, + "referral": { + "title": "Referral Program", + "description1": "Grow the Session Network by inviting others to join the testnet. Share your unique referral link to invite newcomers to claim test SENT tokens and stake to multicontributor nodes.", + "description2": "Learn more here.", + "description3": "By participating in the Testnet Referral Program, you agree to these terms and conditions.", + "showButton": "Show referral link" } }, "sessionNodes": { diff --git a/apps/staking/package.json b/apps/staking/package.json index 14ee74c9..8959a774 100644 --- a/apps/staking/package.json +++ b/apps/staking/package.json @@ -17,13 +17,13 @@ "@session/auth": "workspace:*", "@session/contracts": "workspace:*", "@session/feature-flags": "workspace:*", + "@session/logger": "workspace:*", "@session/sent-staking-js": "workspace:*", "@session/ui": "workspace:*", - "@session/util-js": "workspace:*", "@session/util-crypto": "workspace:*", + "@session/util-js": "workspace:*", "@session/util-logger": "workspace:*", "@session/wallet": "workspace:*", - "@session/logger": "workspace:*", "@tanstack/react-query": "5.51.1", "better-sqlite3-multiple-ciphers": "11.1.2", "class-variance-authority": "^0.7.0", @@ -38,6 +38,7 @@ "rtl-detect": "^1.1.2", "server-only": "^0.0.1", "sharp": "0.32.6", + "sqids": "^0.3.0", "wagmi": "2.12.25", "zod": "^3.23.8" }, diff --git a/apps/staking/testing/data-test-ids.tsx b/apps/staking/testing/data-test-ids.tsx index ffc7bcf4..b8b741ce 100644 --- a/apps/staking/testing/data-test-ids.tsx +++ b/apps/staking/testing/data-test-ids.tsx @@ -29,6 +29,8 @@ export enum ButtonDataTestId { Staked_Node_Exit_Dialog_Submit = 'button:staked-node-exit-dialog-submit', Stake_Amount_Min = 'button:stake-amount-min', Stake_Amount_Max = 'button:stake-amount-max', + Show_Referral_Link = 'button:show-referral-link', + Copy_Referral_Link = 'button:copy-referral-link', } export enum SpecialDataTestId { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d29ce5d..0ad93cff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,6 +228,9 @@ importers: sharp: specifier: 0.32.6 version: 0.32.6 + sqids: + specifier: ^0.3.0 + version: 0.3.0 wagmi: specifier: 2.12.25 version: 2.12.25(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.9)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.51.1)(@tanstack/react-query@5.51.1(react@18.3.1))(@types/react@18.3.9)(bufferutil@4.0.8)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.5(@babel/core@7.24.6))(@types/react@18.3.9)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.21.34(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) @@ -6082,6 +6085,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -9448,6 +9452,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqids@0.3.0: + resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==} + stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -12799,7 +12806,7 @@ snapshots: - react-is - styled-components - '@portabletext/editor@1.1.1(@sanity/block-tools@3.58.0(debug@4.3.4))(@sanity/schema@3.58.0(debug@4.3.4))(@sanity/types@3.58.0)(@sanity/util@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@portabletext/editor@1.1.1(@sanity/block-tools@3.58.0(debug@4.3.4))(@sanity/schema@3.58.0(debug@4.3.4))(@sanity/types@3.58.0(debug@4.3.4))(@sanity/util@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@portabletext/patches': 1.1.0 '@sanity/block-tools': 3.58.0(debug@4.3.4) @@ -14018,7 +14025,7 @@ snapshots: react-copy-to-clipboard: 5.1.0(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - '@sanity/insert-menu@1.0.9(@sanity/types@3.58.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + '@sanity/insert-menu@1.0.9(@sanity/types@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': dependencies: '@sanity/icons': 3.4.0(react@18.3.1) '@sanity/types': 3.58.0 @@ -21352,14 +21359,14 @@ snapshots: '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@dnd-kit/utilities': 3.2.2(react@18.3.1) '@juggle/resize-observer': 3.4.0 - '@portabletext/editor': 1.1.1(@sanity/block-tools@3.58.0(debug@4.3.4))(@sanity/schema@3.58.0(debug@4.3.4))(@sanity/types@3.58.0)(@sanity/util@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@portabletext/editor': 1.1.1(@sanity/block-tools@3.58.0(debug@4.3.4))(@sanity/schema@3.58.0(debug@4.3.4))(@sanity/types@3.58.0(debug@4.3.4))(@sanity/util@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@portabletext/react': 3.1.0(react@18.3.1) '@rexxars/react-json-inspector': 8.0.1(react@18.3.1) '@sanity/asset-utils': 1.3.2 '@sanity/bifur-client': 0.4.1 '@sanity/block-tools': 3.58.0(debug@4.3.4) '@sanity/cli': 3.58.0(react@18.3.1) - '@sanity/client': 6.22.0(debug@4.3.4) + '@sanity/client': 6.22.0 '@sanity/color': 3.0.6 '@sanity/diff': 3.58.0 '@sanity/diff-match-patch': 3.1.1 @@ -21368,14 +21375,14 @@ snapshots: '@sanity/icons': 3.4.0(react@18.3.1) '@sanity/image-url': 1.0.2 '@sanity/import': 3.37.5 - '@sanity/insert-menu': 1.0.9(@sanity/types@3.58.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@sanity/insert-menu': 1.0.9(@sanity/types@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@sanity/logos': 2.1.13(@sanity/color@3.0.6)(react@18.3.1) '@sanity/migrate': 3.58.0 '@sanity/mutator': 3.58.0 '@sanity/presentation': 1.16.5(@sanity/client@6.22.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@sanity/schema': 3.58.0(debug@4.3.4) '@sanity/telemetry': 0.7.9(react@18.3.1) - '@sanity/types': 3.58.0(debug@4.3.4) + '@sanity/types': 3.58.0 '@sanity/ui': 2.8.9(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@sanity/util': 3.58.0(debug@4.3.4) '@sanity/uuid': 3.0.2 @@ -21488,7 +21495,7 @@ snapshots: '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@dnd-kit/utilities': 3.2.2(react@18.3.1) '@juggle/resize-observer': 3.4.0 - '@portabletext/editor': 1.1.1(@sanity/block-tools@3.58.0(debug@4.3.4))(@sanity/schema@3.58.0(debug@4.3.4))(@sanity/types@3.58.0)(@sanity/util@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@portabletext/editor': 1.1.1(@sanity/block-tools@3.58.0(debug@4.3.4))(@sanity/schema@3.58.0(debug@4.3.4))(@sanity/types@3.58.0(debug@4.3.4))(@sanity/util@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rxjs@7.8.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@portabletext/react': 3.1.0(react@18.3.1) '@rexxars/react-json-inspector': 8.0.1(react@18.3.1) '@sanity/asset-utils': 1.3.2 @@ -21504,14 +21511,14 @@ snapshots: '@sanity/icons': 3.4.0(react@18.3.1) '@sanity/image-url': 1.0.2 '@sanity/import': 3.37.5 - '@sanity/insert-menu': 1.0.9(@sanity/types@3.58.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@sanity/insert-menu': 1.0.9(@sanity/types@3.58.0(debug@4.3.4))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@sanity/logos': 2.1.13(@sanity/color@3.0.6)(react@18.3.1) '@sanity/migrate': 3.58.0 '@sanity/mutator': 3.58.0 '@sanity/presentation': 1.16.5(@sanity/client@6.22.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@sanity/schema': 3.58.0(debug@4.3.4) '@sanity/telemetry': 0.7.9(react@18.3.1) - '@sanity/types': 3.58.0 + '@sanity/types': 3.58.0(debug@4.3.4) '@sanity/ui': 2.8.9(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(styled-components@6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@sanity/util': 3.58.0(debug@4.3.4) '@sanity/uuid': 3.0.2 @@ -21928,6 +21935,8 @@ snapshots: sprintf-js@1.0.3: {} + sqids@0.3.0: {} + stack-trace@0.0.10: {} stack-utils@2.0.6: diff --git a/turbo.json b/turbo.json index 1684fe0d..36d80154 100644 --- a/turbo.json +++ b/turbo.json @@ -36,7 +36,8 @@ "NEXT_PUBLIC_SANITY_PROJECT_ID", "NEXT_PUBLIC_SANITY_DATASET", "NEXT_PUBLIC_SANITY_API_VERSION", - "SANITY_REVALIDATE_SECRET" + "SANITY_REVALIDATE_SECRET", + "NEXT_PUBLIC_POINTS_PROGRAM_API" ] }, "dev": { From 067fc86565fd7ca20b84c05a940c6e5f9404a3e5 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 7 Nov 2024 16:08:15 +1100 Subject: [PATCH 2/4] fix: move claim button --- apps/staking/app/mystakes/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/staking/app/mystakes/page.tsx b/apps/staking/app/mystakes/page.tsx index 122158f1..53ae755b 100644 --- a/apps/staking/app/mystakes/page.tsx +++ b/apps/staking/app/mystakes/page.tsx @@ -30,9 +30,9 @@ export default function Page() { - + {chain === CHAIN.TESTNET ? : }
From dded84a3dc080a32869375580aa4ff209d4d5d8e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 7 Nov 2024 16:50:17 +1100 Subject: [PATCH 3/4] fix: remove hide on mobile for ref --- apps/staking/app/mystakes/modules/ReferralModule.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/staking/app/mystakes/modules/ReferralModule.tsx b/apps/staking/app/mystakes/modules/ReferralModule.tsx index f38c48af..00444778 100644 --- a/apps/staking/app/mystakes/modules/ReferralModule.tsx +++ b/apps/staking/app/mystakes/modules/ReferralModule.tsx @@ -29,7 +29,7 @@ export default function ReferralModule() { }, [address]); return ( - + {dictionary('title')}

From 498b631b744c0d87ab8872e5d01c749467222499 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 7 Nov 2024 17:19:26 +1100 Subject: [PATCH 4/4] fix: ref fixes --- apps/staking/app/faucet/actions.ts | 12 ++------ apps/staking/app/faucet/utils.tsx | 35 ++++++++++-------------- apps/staking/components/ActionModule.tsx | 2 +- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/apps/staking/app/faucet/actions.ts b/apps/staking/app/faucet/actions.ts index 57b87e43..b2ab6095 100644 --- a/apps/staking/app/faucet/actions.ts +++ b/apps/staking/app/faucet/actions.ts @@ -1,14 +1,7 @@ 'use server'; import { COMMUNITY_DATE, FAUCET, FAUCET_ERROR, TICKER } from '@/lib/constants'; -import { - addresses, - CHAIN, - formatSENT, - isChain, - SENT_DECIMALS, - SENT_SYMBOL, -} from '@session/contracts'; +import { addresses, CHAIN, isChain, SENT_DECIMALS, SENT_SYMBOL } from '@session/contracts'; import { SENTAbi } from '@session/contracts/abis'; import { ETH_DECIMALS } from '@session/wallet/lib/eth'; import { createPublicWalletClient, createServerWallet } from '@session/wallet/lib/server-wallet'; @@ -28,6 +21,7 @@ import { TABLE, TransactionHistory, } from './utils'; +import { formatSENTBigInt } from '@session/contracts/hooks/SENT'; class FaucetError extends Error { faucetError: FAUCET_ERROR; @@ -196,7 +190,7 @@ export async function transferTestTokens({ */ if (faucetTokenBalance < faucetTokenWarning) { console.warn( - `Faucet wallet ${SENT_SYMBOL} balance (${formatSENT(faucetTokenBalance)} ${SENT_SYMBOL}) is below the warning threshold (${formatSENT(faucetTokenWarning)})` + `Faucet wallet ${SENT_SYMBOL} balance (${formatSENTBigInt(faucetTokenBalance)}) is below the warning threshold (${formatSENTBigInt(faucetTokenWarning)})` ); } diff --git a/apps/staking/app/faucet/utils.tsx b/apps/staking/app/faucet/utils.tsx index 27561bf8..7b0e3136 100644 --- a/apps/staking/app/faucet/utils.tsx +++ b/apps/staking/app/faucet/utils.tsx @@ -2,7 +2,6 @@ import Database, * as BetterSql3 from 'better-sqlite3-multiple-ciphers'; import path from 'path'; import { Address } from 'viem'; import { isProduction } from '@/lib/env'; -import { encodeAddressToHashId } from '@/lib/hashid'; export enum TABLE { TRANSACTIONS = 'transactions', @@ -26,8 +25,8 @@ export enum TRANSACTIONS_TABLE { DISCORD = 'discord', TELEGRAM = 'telegram', OPERATOR = 'operator', - WALLET = 'wallet', CODE = 'code', + WALLET = 'wallet', ETHHASH = 'ethhash', ETHAMOUNT = 'ethamount', } @@ -677,24 +676,20 @@ export function createWalletReferralCodes({ params }: CreateWalletReferralCodesP } } -const test: Array<{ address: Address; uses: number }> = [ - { address: '0x501bcb8d257b8115dc6c5048bA34A4B9E668d46e', uses: 1 }, - { address: '0xd2A954dE41b04E999d1B94dB56B27AFaF4389053', uses: 1 }, - { address: '0x9EbDB42452eA9c01BeBaA4955ad3b875C461E065', uses: 1 }, - { address: '0x6Be297b19d6dEDDC4Cd61a54A01f054670F8703E', uses: 2 }, - { address: '0x349c2968926fdE3bb357eBD92519cE5Ec35E270A', uses: 3 }, - { address: '0xEaA90e3f8801B9797435C6A026deB1f4A018B83f', uses: 4 }, -]; - -export function createWalletCodes() { - const params: Array = test.map(({ address, uses }) => ({ - codes: new Set([encodeAddressToHashId(address)]), - creatorWallet: address, - maxUses: uses, - drip: 5000000000000n, - })); - createWalletReferralCodes({ params }); -} +// +// const test: Array<{ address: Address; uses: number }> = [ +// +// ]; +// +// export function createWalletCodes() { +// const params: Array = test.map(({ address, uses }) => ({ +// codes: new Set([encodeAddressToHashId(address)]), +// creatorWallet: address, +// maxUses: uses, +// drip: 5000000000000n, +// })); +// createWalletReferralCodes({ params }); +// } /* interface NodeOperatorScoreResponse { diff --git a/apps/staking/components/ActionModule.tsx b/apps/staking/components/ActionModule.tsx index 170adcfa..137d4210 100644 --- a/apps/staking/components/ActionModule.tsx +++ b/apps/staking/components/ActionModule.tsx @@ -47,7 +47,7 @@ export default function ActionModule({