Skip to content

Commit

Permalink
Merge pull request #4 from session-foundation/testnet_referral
Browse files Browse the repository at this point in the history
Testnet referral program
  • Loading branch information
Aerilym authored Nov 7, 2024
2 parents 5544a35 + 498b631 commit d2472ce
Show file tree
Hide file tree
Showing 19 changed files with 802 additions and 115 deletions.
3 changes: 2 additions & 1 deletion apps/staking/.env.local.template
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
TELEGRAM_BOT_TOKEN=
NEXTAUTH_SECRET=
NEXTAUTH_URL=
NEXTAUTH_URL=
NEXT_PUBLIC_POINTS_PROGRAM_API=
20 changes: 15 additions & 5 deletions apps/staking/app/faucet/AuthModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@ export const getFaucetFormSchema = () => {
}),
discordId: z.string().optional(),
telegramId: z.string().optional(),
code: z.string().optional(),
});
};

export type FaucetFormSchema = z.infer<ReturnType<typeof getFaucetFormSchema>>;

export const AuthModule = () => {
export const AuthModule = ({ code }: { code?: string }) => {
const dictionary = useTranslations('faucet.form');
const generalDictionary = useTranslations('general');
const [submitAttemptCounter, setSubmitAttemptCounter] = useState<number>(0);
Expand All @@ -88,6 +89,7 @@ export const AuthModule = () => {
walletAddress: '',
discordId: '',
telegramId: '',
code: code ?? '',
},
reValidateMode: 'onChange',
});
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -345,10 +353,12 @@ export const AuthModule = () => {
<>
<span className="text-center">- {generalDictionary('or')} -</span>
<WalletModalButtonWithLocales rounded="md" size="lg" className="uppercase" hideBalance />
<span className="inline-flex w-full flex-col gap-2 uppercase xl:flex-row [&>*]:flex-grow">
{!isConnected || (isConnected && discordId) ? <DiscordAuthButton /> : null}
{!isConnected || (isConnected && telegramId) ? <TelegramAuthButton /> : null}
</span>
{!code ? (
<span className="inline-flex w-full flex-col gap-2 uppercase xl:flex-row [&>*]:flex-grow">
{!isConnected || (isConnected && discordId) ? <DiscordAuthButton /> : null}
{!isConnected || (isConnected && telegramId) ? <TelegramAuthButton /> : null}
</span>
) : null}
</>
) : null}

Expand Down
44 changes: 44 additions & 0 deletions apps/staking/app/faucet/Faucet.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<NextAuthProvider>
<div className="lg:-mt-header-displacement max-w-screen-3xl mx-auto flex w-screen flex-col-reverse items-center justify-around gap-16 px-4 py-16 align-middle xl:grid xl:h-dvh xl:grid-cols-2 xl:p-32 xl:py-0">
<div className="flex h-max flex-col gap-4 text-start">
<h1 className="text-5xl font-semibold">{dictionary('title')}</h1>
<h2 className="text-lg font-semibold">{dictionary('communityTitle')}</h2>
<p>
{dictionary.rich('communityDescription', {
connectionOptions: formatList(['Discord', 'Telegram']),
snapshotDate: formatDate(new Date(COMMUNITY_DATE.SESSION_TOKEN_COMMUNITY_SNAPSHOT), {
dateStyle: 'long',
}),
})}
</p>
<h2 className="text-lg font-semibold">{dictionary('oxenTitle')}</h2>
<p>
{dictionary.rich('oxenDescription', {
oxenRegistrationDate: formatDate(
new Date(COMMUNITY_DATE.OXEN_SERVICE_NODE_BONUS_PROGRAM),
{
dateStyle: 'long',
}
),
})}
</p>
<p>{dictionary('notEligible')}</p>
<h2 className="text-lg font-semibold">{dictionary('walletRequirementTitle')}</h2>
<p>{dictionary.rich('walletRequirementDescription')}</p>
</div>
<div className="h-max min-h-[400px]">
<AuthModule code={code} />
</div>
</div>
</NextAuthProvider>
);
}
14 changes: 14 additions & 0 deletions apps/staking/app/faucet/[code]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <Faucet code={code} />;
}
147 changes: 109 additions & 38 deletions apps/staking/app/faucet/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'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, 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';
Expand All @@ -10,6 +10,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,
Expand Down Expand Up @@ -70,7 +73,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));

Expand Down Expand Up @@ -132,24 +134,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 }),
Expand Down Expand Up @@ -187,21 +197,70 @@ 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 (
!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)),
Expand All @@ -210,22 +269,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.
Expand All @@ -240,7 +308,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',
Expand All @@ -254,7 +322,7 @@ export async function transferTestTokens({
) {
throw new FaucetError(
FAUCET_ERROR.ALREADY_USED_SERVICE,
dictionary('alreadyUsedService', {
dictionary(FAUCET_ERROR.ALREADY_USED_SERVICE, {
service: 'Discord',
})
);
Expand All @@ -273,7 +341,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',
Expand All @@ -292,7 +360,7 @@ export async function transferTestTokens({
) {
throw new FaucetError(
FAUCET_ERROR.ALREADY_USED_SERVICE,
dictionary('alreadyUsedService', {
dictionary(FAUCET_ERROR.ALREADY_USED_SERVICE, {
service: 'Telegram',
})
);
Expand All @@ -312,21 +380,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,
Expand All @@ -336,6 +405,8 @@ export async function transferTestTokens({
discordId,
telegramId,
usedOperatorAddress ? targetAddress : undefined,
usedWalletListAddress ? targetAddress : undefined,
usedCode ? code : undefined,
ethTxHash ?? null,
ethTopupValue.toString()
);
Expand Down
Loading

0 comments on commit d2472ce

Please sign in to comment.