diff --git a/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx b/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx
index 9a2d71a8ee..5af3e7d881 100644
--- a/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx
+++ b/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx
@@ -12,7 +12,6 @@ import Fieldset from 'apps/web/src/components/Fieldset';
import { Icon } from 'apps/web/src/components/Icon/Icon';
import Label from 'apps/web/src/components/Label';
import TransactionError from 'apps/web/src/components/TransactionError';
-import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
import useWriteBaseEnsTextRecords from 'apps/web/src/hooks/useWriteBaseEnsTextRecords';
import {
UsernameTextRecordKeys,
@@ -33,14 +32,10 @@ export default function RegistrationProfileForm() {
const [currentFormStep, setCurrentFormStep] = useState(FormSteps.Description);
const [transitionStep, setTransitionStep] = useState(false);
const { logError } = useErrors();
- const { redirectToProfile } = useRegistration();
+ const { redirectToProfile, selectedNameFormatted } = useRegistration();
const { address } = useAccount();
const { logEventWithContext } = useAnalytics();
- const { data: baseEnsName } = useBaseEnsName({
- address,
- });
-
const {
updateTextRecords,
updatedTextRecords,
@@ -49,7 +44,7 @@ export default function RegistrationProfileForm() {
writeTextRecordsError,
} = useWriteBaseEnsTextRecords({
address: address,
- username: baseEnsName,
+ username: selectedNameFormatted,
onSuccess: () => {
redirectToProfile();
},
@@ -108,37 +103,37 @@ export default function RegistrationProfileForm() {
const descriptionLabelChildren = (
);
const socialsLabelChildren = (
-
+
Add Socials
Step 2 of 3
-
+
);
const keywordsLabelChildren = (
-
+
Add areas of expertise
Step 3 of 3
-
+
);
diff --git a/apps/web/src/hooks/useAggregatedDiscountValidators.ts b/apps/web/src/hooks/useAggregatedDiscountValidators.ts
index 382d1624f8..15b241d348 100644
--- a/apps/web/src/hooks/useAggregatedDiscountValidators.ts
+++ b/apps/web/src/hooks/useAggregatedDiscountValidators.ts
@@ -7,6 +7,7 @@ import {
useCheckCBIDAttestations,
useCheckCoinbaseAttestations,
useCheckEAAttestations,
+ useDiscountCodeAttestations,
useSummerPassAttestations,
} from 'apps/web/src/hooks/useAttestations';
import { useActiveDiscountValidators } from 'apps/web/src/hooks/useReadActiveDiscountValidators';
@@ -37,7 +38,7 @@ export function findFirstValidDiscount(
return sortedDiscounts.find((data) => data?.discountKey) ?? undefined;
}
-export function useAggregatedDiscountValidators() {
+export function useAggregatedDiscountValidators(code?: string) {
const { data: activeDiscountValidators, isLoading: loadingActiveDiscounts } =
useActiveDiscountValidators();
const { data: CBIDData, loading: loadingCBIDAttestations } = useCheckCBIDAttestations();
@@ -49,6 +50,8 @@ export function useAggregatedDiscountValidators() {
const { data: BuildathonData, loading: loadingBuildathon } = useBuildathonAttestations();
const { data: BaseDotEthData, loading: loadingBaseDotEth } = useBaseDotEthAttestations();
const { data: BNSData, loading: loadingBNS } = useBNSAttestations();
+ const { data: DiscountCodeData, loading: loadingDiscountCode } =
+ useDiscountCodeAttestations(code);
const loadingDiscounts =
loadingCoinbaseAttestations ||
@@ -59,7 +62,8 @@ export function useAggregatedDiscountValidators() {
loadingBuildathon ||
loadingSummerPass ||
loadingBaseDotEth ||
- loadingBNS;
+ loadingBNS ||
+ loadingDiscountCode;
const discountsToAttestationData = useMemo(() => {
const discountMapping: MappedDiscountData = {};
@@ -114,6 +118,16 @@ export function useAggregatedDiscountValidators() {
if (BNSData && validator.discountValidator === BNSData.discountValidatorAddress) {
discountMapping[Discount.BNS_NAME] = { ...BNSData, discountKey: validator.key };
}
+
+ if (
+ DiscountCodeData &&
+ validator.discountValidator === DiscountCodeData.discountValidatorAddress
+ ) {
+ discountMapping[Discount.DISCOUNT_CODE] = {
+ ...DiscountCodeData,
+ discountKey: validator.key,
+ };
+ }
});
return discountMapping;
@@ -127,6 +141,7 @@ export function useAggregatedDiscountValidators() {
SummerPassData,
BaseDotEthData,
BNSData,
+ DiscountCodeData,
]);
return {
diff --git a/apps/web/src/hooks/useAttestations.ts b/apps/web/src/hooks/useAttestations.ts
index c92b4ff7bb..a950bc9c5e 100644
--- a/apps/web/src/hooks/useAttestations.ts
+++ b/apps/web/src/hooks/useAttestations.ts
@@ -1,5 +1,6 @@
import { useErrors } from 'apps/web/contexts/Errors';
import { CoinbaseProofResponse } from 'apps/web/pages/api/proofs/coinbase';
+import { DiscountCodeResponse } from 'apps/web/pages/api/proofs/discountCode';
import AttestationValidatorABI from 'apps/web/src/abis/AttestationValidator';
import CBIDValidatorABI from 'apps/web/src/abis/CBIdDiscountValidator';
import EarlyAccessValidatorABI from 'apps/web/src/abis/EarlyAccessValidator';
@@ -487,3 +488,71 @@ export function useBNSAttestations() {
}
return { data: null, loading: isLoading, error };
}
+
+// returns info about Discount Codes attestations
+export function useDiscountCodeAttestations(code?: string) {
+ const { logError } = useErrors();
+ const { address } = useAccount();
+ const [loading, setLoading] = useState(false);
+ const [discountCodeResponse, setDiscountCodeResponse] = useState(
+ null,
+ );
+
+ const { basenameChain } = useBasenameChain();
+
+ useEffect(() => {
+ async function checkDiscountCode(a: string, c: string) {
+ try {
+ setLoading(true);
+ const params = new URLSearchParams();
+ params.append('address', a);
+ params.append('chain', basenameChain.id.toString());
+ params.append('code', c.toString());
+ const response = await fetch(`/api/proofs/discountCode?${params}`);
+ const result = (await response.json()) as DiscountCodeResponse;
+ if (response.ok) {
+ setDiscountCodeResponse(result);
+ }
+ } catch (error) {
+ logError(error, 'Error checking Discount code');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (address && !IS_EARLY_ACCESS && !!code) {
+ checkDiscountCode(address, code).catch((error) => {
+ logError(error, 'Error checking Discount code');
+ });
+ }
+ }, [address, basenameChain.id, code, logError]);
+
+ const signature = discountCodeResponse?.signedMessage;
+ const readContractArgs = useMemo(() => {
+ if (!address || !signature || !code) {
+ return {};
+ }
+
+ return {
+ address: discountCodeResponse?.discountValidatorAddress,
+ abi: AttestationValidatorABI,
+ functionName: 'isValidDiscountRegistration',
+ args: [address, signature],
+ };
+ }, [address, code, discountCodeResponse?.discountValidatorAddress, signature]);
+
+ const { data: isValid, isLoading, error } = useReadContract(readContractArgs);
+
+ if (isValid && discountCodeResponse && address && signature) {
+ return {
+ data: {
+ discountValidatorAddress: discountCodeResponse.discountValidatorAddress,
+ discount: Discount.DISCOUNT_CODE,
+ validationData: signature,
+ },
+ loading: false,
+ error: null,
+ };
+ }
+ return { data: null, loading: loading || isLoading, error };
+}
diff --git a/apps/web/src/hooks/useCapabilitiesSafe.ts b/apps/web/src/hooks/useCapabilitiesSafe.ts
index 78fe3a3f2e..09f9c419ba 100644
--- a/apps/web/src/hooks/useCapabilitiesSafe.ts
+++ b/apps/web/src/hooks/useCapabilitiesSafe.ts
@@ -9,15 +9,16 @@
*/
import { Chain } from 'viem';
+import { base } from 'viem/chains';
import { useAccount } from 'wagmi';
import { useCapabilities } from 'wagmi/experimental';
export type UseCapabilitiesSafeProps = {
- chain: Chain;
+ chainId?: Chain['id'];
};
-export default function useCapabilitiesSafe({ chain }: UseCapabilitiesSafeProps) {
- const { connector, isConnected } = useAccount();
+export default function useCapabilitiesSafe({ chainId }: UseCapabilitiesSafeProps) {
+ const { connector, isConnected, chainId: currentChainId } = useAccount();
// Metamask doesn't support wallet_getCapabilities
const isMetamaskWallet = connector?.id === 'io.metamask';
@@ -25,10 +26,12 @@ export default function useCapabilitiesSafe({ chain }: UseCapabilitiesSafeProps)
const { data: capabilities } = useCapabilities({ query: { enabled } });
+ const featureChainId = chainId ?? currentChainId ?? base.id;
+
const atomicBatchEnabled =
- (capabilities && capabilities[chain.id]?.atomicBatch?.supported === true) ?? false;
+ (capabilities && capabilities[featureChainId]?.atomicBatch?.supported === true) ?? false;
const paymasterServiceEnabled =
- (capabilities && capabilities[chain.id]?.paymasterService?.supported === true) ?? false;
+ (capabilities && capabilities[featureChainId]?.paymasterService?.supported === true) ?? false;
return {
atomicBatchEnabled,
diff --git a/apps/web/src/hooks/useRegisterNameCallback.ts b/apps/web/src/hooks/useRegisterNameCallback.ts
index adc800724f..f812f1b08f 100644
--- a/apps/web/src/hooks/useRegisterNameCallback.ts
+++ b/apps/web/src/hooks/useRegisterNameCallback.ts
@@ -1,9 +1,11 @@
-import { useAnalytics } from 'apps/web/contexts/Analytics';
import { useErrors } from 'apps/web/contexts/Errors';
import L2ResolverAbi from 'apps/web/src/abis/L2Resolver';
import { USERNAME_L2_RESOLVER_ADDRESSES } from 'apps/web/src/addresses/usernames';
import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
+import useCapabilitiesSafe from 'apps/web/src/hooks/useCapabilitiesSafe';
+import useWriteContractsWithLogs from 'apps/web/src/hooks/useWriteContractsWithLogs';
+import useWriteContractWithReceipt from 'apps/web/src/hooks/useWriteContractWithReceipt';
import {
formatBaseEthDomain,
IS_EARLY_ACCESS,
@@ -11,43 +13,30 @@ import {
REGISTER_CONTRACT_ABI,
REGISTER_CONTRACT_ADDRESSES,
} from 'apps/web/src/utils/usernames';
-import { ActionType } from 'libs/base-ui/utils/logEvent';
-import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { encodeFunctionData, namehash } from 'viem';
-import { useAccount, useSwitchChain, useWriteContract } from 'wagmi';
-import { useCapabilities, useWriteContracts } from 'wagmi/experimental';
+import { useAccount } from 'wagmi';
function secondsInYears(years: number): bigint {
const secondsPerYear = 365.25 * 24 * 60 * 60; // .25 accounting for leap years
return BigInt(Math.round(years * secondsPerYear));
}
-type UseRegisterNameCallbackReturnValue = {
- callback: () => Promise;
- data: `0x${string}` | undefined;
- isPending: boolean;
- error: string | undefined | null;
- reverseRecord: boolean;
- setReverseRecord: Dispatch>;
- hasExistingBasename: boolean;
-};
-
export function useRegisterNameCallback(
name: string,
value: bigint | undefined,
years: number,
discountKey?: `0x${string}`,
validationData?: `0x${string}`,
-): UseRegisterNameCallbackReturnValue {
- const { address, chainId, isConnected, connector } = useAccount();
+) {
+ const { address } = useAccount();
const { basenameChain } = useBasenameChain();
const { logError } = useErrors();
- const {
- writeContractsAsync,
- isPending: paymasterIsPending,
- error: paymasterError,
- } = useWriteContracts();
+ const { paymasterServiceEnabled } = useCapabilitiesSafe({
+ chainId: basenameChain.id,
+ });
+ // If user has a basename, reverse record is set to false
const { data: baseEnsName, isLoading: baseEnsNameIsLoading } = useBaseEnsName({
address,
});
@@ -59,41 +48,31 @@ export function useRegisterNameCallback(
const [reverseRecord, setReverseRecord] = useState(!hasExistingBasename);
- const isCoinbaseSmartWallet = connector?.id === 'coinbase';
- const paymasterEnabled = isCoinbaseSmartWallet;
+ // Transaction with paymaster enabled
+ const { initiateBatchCalls, batchCallsStatus, batchCallsIsLoading, batchCallsError } =
+ useWriteContractsWithLogs({
+ chain: basenameChain,
+ eventName: 'register_name',
+ });
- const { data, writeContractAsync, isPending, error } = useWriteContract();
- const { data: availableCapacities } = useCapabilities({
- account: address,
- query: { enabled: isConnected && paymasterEnabled },
+ // Transaction without paymaster
+ const {
+ initiateTransaction: initiateRegisterName,
+ transactionStatus: registerNameStatus,
+ transactionIsLoading: registerNameIsLoading,
+ transactionError: registerNameError,
+ } = useWriteContractWithReceipt({
+ chain: basenameChain,
+ eventName: 'register_name',
});
- const capabilities = useMemo(() => {
- if (!isConnected || !chainId || !availableCapacities) {
- return {};
- }
- const chainCapabilities = availableCapacities[chainId];
- if (chainCapabilities.paymasterService?.supported) {
- return {
- paymasterService: {
- // url: `${document.location.origin}/api/paymaster`
- },
- };
- }
- return {};
- }, [availableCapacities, chainId, isConnected]);
-
+ // Params
const normalizedName = normalizeEnsDomainName(name);
- const { switchChainAsync } = useSwitchChain();
const isDiscounted = Boolean(discountKey && validationData);
- const { logEventWithContext } = useAnalytics();
+ // Callback
const registerName = useCallback(async () => {
if (!address) return;
- if (chainId !== basenameChain.id) {
- await switchChainAsync({ chainId: basenameChain.id });
- return;
- }
const addressData = encodeFunctionData({
abi: L2ResolverAbi,
@@ -119,22 +98,17 @@ export function useRegisterNameCallback(
reverseRecord, // Bool to decide whether to set this name as the "primary" name for the `owner`.
};
- // Log attempt to register name
- logEventWithContext('register_name_transaction_initiated', ActionType.click);
-
try {
- if (!capabilities || Object.keys(capabilities).length === 0) {
- await writeContractAsync({
+ if (!paymasterServiceEnabled) {
+ await initiateRegisterName({
abi: REGISTER_CONTRACT_ABI,
address: REGISTER_CONTRACT_ADDRESSES[basenameChain.id],
- chainId: basenameChain.id,
functionName: isDiscounted || IS_EARLY_ACCESS ? 'discountedRegister' : 'register',
- // @ts-expect-error isDiscounted is sufficient guard for discountKey and validationData presence
args: isDiscounted ? [registerRequest, discountKey, validationData] : [registerRequest],
value,
});
} else {
- await writeContractsAsync({
+ await initiateBatchCalls({
contracts: [
{
abi: REGISTER_CONTRACT_ABI,
@@ -143,46 +117,41 @@ export function useRegisterNameCallback(
args: isDiscounted
? [registerRequest, discountKey, validationData]
: [registerRequest],
- // @ts-expect-error writeContractsAsync is incorrectly typed to not accept value
value,
},
],
- capabilities: capabilities,
- chainId: basenameChain.id,
+ account: address,
+ chain: basenameChain,
});
}
} catch (e) {
logError(e, 'Register name transaction canceled');
- logEventWithContext('register_name_transaction_canceled', ActionType.change);
}
}, [
address,
- chainId,
- basenameChain.id,
+ basenameChain,
+ discountKey,
+ initiateBatchCalls,
+ initiateRegisterName,
+ isDiscounted,
+ logError,
name,
normalizedName,
- years,
+ paymasterServiceEnabled,
reverseRecord,
- logEventWithContext,
- switchChainAsync,
- capabilities,
- writeContractAsync,
- isDiscounted,
- discountKey,
validationData,
value,
- writeContractsAsync,
- logError,
+ years,
]);
return {
callback: registerName,
- data,
- isPending: isPending ?? paymasterIsPending,
- // @ts-expect-error error will be string renderable
- error: error ?? paymasterError,
+ isPending: registerNameIsLoading || batchCallsIsLoading,
+ error: registerNameError ?? batchCallsError,
reverseRecord,
setReverseRecord,
hasExistingBasename,
+ batchCallsStatus,
+ registerNameStatus,
};
}
diff --git a/apps/web/src/hooks/useWriteContractsWithLogs.ts b/apps/web/src/hooks/useWriteContractsWithLogs.ts
index 4374e38410..5628e2ed60 100644
--- a/apps/web/src/hooks/useWriteContractsWithLogs.ts
+++ b/apps/web/src/hooks/useWriteContractsWithLogs.ts
@@ -55,7 +55,7 @@ export default function useWriteContractsWithLogs({
// Errors & Analytics
const { logEventWithContext } = useAnalytics();
const { logError } = useErrors();
- const { atomicBatchEnabled } = useCapabilitiesSafe({ chain });
+ const { atomicBatchEnabled } = useCapabilitiesSafe({ chainId: chain.id });
const { chain: connectedChain } = useAccount();
const [batchCallsStatus, setBatchCallsStatus] = useState(BatchCallsStatus.Idle);
@@ -71,7 +71,7 @@ export default function useWriteContractsWithLogs({
} = useWriteContracts();
// Experimental: Track batch call status
- const { data: sendCallsResult, isPending: sendCallsResultIsPending } = useCallsStatus({
+ const { data: sendCallsResult, isFetching: sendCallsResultIsFetching } = useCallsStatus({
// @ts-expect-error: We can expect sendCallsId to be undefined since we're only enabling the query when defined
id: sendCallsId,
query: {
@@ -179,7 +179,7 @@ export default function useWriteContractsWithLogs({
const batchCallsIsLoading =
sendCallsIsPending ||
transactionReceiptIsFetching ||
- sendCallsResultIsPending ||
+ sendCallsResultIsFetching ||
sendCallsResult?.status === 'PENDING';
const batchCallsIsSuccess = sendCallsIsSuccess && transactionReceiptIsSuccess;
const batchCallsIsError = sendCallsIsError || transactionReceiptIsError;
diff --git a/apps/web/src/utils/proofs/discount_code_storage.ts b/apps/web/src/utils/proofs/discount_code_storage.ts
new file mode 100644
index 0000000000..806468c3dc
--- /dev/null
+++ b/apps/web/src/utils/proofs/discount_code_storage.ts
@@ -0,0 +1,38 @@
+import { createKysely } from '@vercel/postgres-kysely';
+
+type DiscountCodesTable = {
+ code: string;
+ expires_at: Date;
+ usage_limit: number;
+ usage_count: number;
+};
+
+type Database = {
+ 'public.basenames_discount_codes': DiscountCodesTable;
+};
+
+export enum DiscountCodeTableNamespace {
+ DiscountCodes = 'basenames_discount_codes',
+}
+const publicTableName = 'public.basenames_discount_codes';
+
+export async function getDiscountCode(code: string) {
+ let query = createKysely().selectFrom(publicTableName).where('code', 'ilike', code);
+ return query.selectAll().limit(1).execute();
+}
+
+export async function incrementDiscountCodeUsage(code: string) {
+ const db = createKysely();
+ const tableName = publicTableName;
+
+ // Perform the update and return the updated row in a single query
+ const result = await db
+ .updateTable(tableName)
+ .set((eb) => ({
+ usage_count: eb('usage_count', '+', 1),
+ }))
+ .where('code', 'ilike', code)
+ .executeTakeFirst();
+
+ return result;
+}
diff --git a/apps/web/src/utils/proofs/sybil_resistance.ts b/apps/web/src/utils/proofs/sybil_resistance.ts
index 301e7fd583..f4dea8d3fe 100644
--- a/apps/web/src/utils/proofs/sybil_resistance.ts
+++ b/apps/web/src/utils/proofs/sybil_resistance.ts
@@ -5,6 +5,7 @@ import RegistrarControllerABI from 'apps/web/src/abis/RegistrarControllerABI';
import {
USERNAME_CB1_DISCOUNT_VALIDATORS,
USERNAME_CB_DISCOUNT_VALIDATORS,
+ USERNAME_DISCOUNT_CODE_VALIDATORS,
USERNAME_EA_DISCOUNT_VALIDATORS,
} from 'apps/web/src/addresses/usernames';
import { getLinkedAddresses } from 'apps/web/src/cdp/api';
@@ -66,6 +67,9 @@ const discountTypes: DiscountTypesByChainId = {
[DiscountType.EARLY_ACCESS]: {
discountValidatorAddress: USERNAME_EA_DISCOUNT_VALIDATORS[baseSepolia.id],
},
+ [DiscountType.DISCOUNT_CODE]: {
+ discountValidatorAddress: USERNAME_DISCOUNT_CODE_VALIDATORS[baseSepolia.id],
+ },
},
};
@@ -83,6 +87,22 @@ export async function hasRegisteredWithDiscount(
});
}
+async function getMessageSignature(message: `0x${string}`) {
+ // hash the message
+ const msgHash = keccak256(message);
+
+ // sign the hashed message
+ const { r, s, v } = await sign({
+ hash: msgHash,
+ privateKey: `0x${trustedSignerPKey}`,
+ });
+
+ // combine r, s, and v into a single signature
+ const signature = `${r.slice(2)}${s.slice(2)}${(v as bigint).toString(16)}`;
+
+ return signature;
+}
+
async function signMessageWithTrustedSigner(
claimerAddress: Address,
targetAddress: Address,
@@ -97,17 +117,7 @@ async function signMessageWithTrustedSigner(
['0x1900', targetAddress, trustedSignerAddress, claimerAddress, BigInt(expiry)],
);
- // hash the message
- const msgHash = keccak256(message);
-
- // sign the hashed message
- const { r, s, v } = await sign({
- hash: msgHash,
- privateKey: `0x${trustedSignerPKey}`,
- });
-
- // combine r, s, and v into a single signature
- const signature = `${r.slice(2)}${s.slice(2)}${(v as bigint).toString(16)}`;
+ const signature = await getMessageSignature(message);
// return the encoded signed message
return encodeAbiParameters(parseAbiParameters('address, uint64, bytes'), [
@@ -117,6 +127,31 @@ async function signMessageWithTrustedSigner(
]);
}
+export async function signDiscountMessageWithTrustedSigner(
+ claimerAddress: Address,
+ couponCodeUuid: Address,
+ targetAddress: Address,
+ expiry: number,
+) {
+ if (!trustedSignerAddress || !isAddress(trustedSignerAddress)) {
+ throw new Error('Must provide a valid trustedSignerAddress');
+ }
+
+ const message = encodePacked(
+ ['bytes2', 'address', 'address', 'address', 'bytes32', 'uint64'],
+ ['0x1900', targetAddress, trustedSignerAddress, claimerAddress, couponCodeUuid, BigInt(expiry)],
+ );
+
+ const signature = await getMessageSignature(message);
+
+ // return the encoded signed message
+ return encodeAbiParameters(parseAbiParameters('uint64, bytes32, bytes'), [
+ BigInt(expiry),
+ couponCodeUuid,
+ `0x${signature}`,
+ ]);
+}
+
export async function sybilResistantUsernameSigning(
address: `0x${string}`,
discountType: DiscountType,
diff --git a/apps/web/src/utils/proofs/types.ts b/apps/web/src/utils/proofs/types.ts
index 8fdc957dd8..9395a5246f 100644
--- a/apps/web/src/utils/proofs/types.ts
+++ b/apps/web/src/utils/proofs/types.ts
@@ -21,6 +21,7 @@ export enum DiscountType {
CB = 'CB',
CB1 = 'CB1',
CB_ID = 'CB_ID',
+ DISCOUNT_CODE = 'DISCOUNT_CODE',
}
export type DiscountValue = {
diff --git a/apps/web/src/utils/usernames.ts b/apps/web/src/utils/usernames.ts
index d15562e491..417a3f26f5 100644
--- a/apps/web/src/utils/usernames.ts
+++ b/apps/web/src/utils/usernames.ts
@@ -391,6 +391,7 @@ export enum Discount {
SUMMER_PASS_LVL_3 = 'SUMMER_PASS_LVL_3',
BNS_NAME = 'BNS_NAME',
BASE_DOT_ETH_NFT = 'BASE_DOT_ETH_NFT',
+ DISCOUNT_CODE = 'DISCOUNT_CODE',
}
export function isValidDiscount(key: string): key is keyof typeof Discount {