feat: add BNS discount (#806)

* add BNS discount

* add sepolia validator address

* fix api handler type
This commit is contained in:
Jordan Frankfurt
2024-08-08 14:04:02 -05:00
committed by GitHub
parent 833a4c7ff5
commit 26ef77ed4c
8 changed files with 207 additions and 24 deletions

View File

@@ -61,7 +61,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(404).json({ error: 'address is not eligible for early access' });
}
const responseData: EarlyAccessProofResponse = {
const responseData: BNSProofResponse = {
...content,
proofs,
discountValidatorAddress: USERNAME_BNS_DISCOUNT_VALIDATORS[parsedChain],

View File

@@ -39,7 +39,7 @@ export const USERNAME_EA_DISCOUNT_VALIDATORS: AddressMap = {
};
export const USERNAME_BNS_DISCOUNT_VALIDATORS: AddressMap = {
[baseSepolia.id]: '0x',
[baseSepolia.id]: '0x1DE649d8b004A44491a7D3ebbb23F4B0DA89DE78',
[base.id]: '0x',
};

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<g clip-path="url(#clip0_8972_15511)">
<rect width="32" height="32" rx="8" fill="#0052FF" />
<path
d="M15.9808 27C22.0665 27 27 22.0751 27 16C27 9.92487 22.0665 5 15.9808 5C10.207 5 5.47042 9.43292 5 15.0754H19.5648V16.9246H5C5.47042 22.5671 10.207 27 15.9808 27Z"
fill="white" />
</g>
<defs>
<clipPath id="clip0_8972_15511">
<rect width="32" height="32" rx="8" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -8,6 +8,8 @@ import Link from 'next/link';
import baseBuildathonParticipant from './images/base-buildathon-participant.svg';
import summerPassLvl3 from './images/summer-pass-lvl-3.svg';
import cbidVerification from './images/cbid-verification.svg';
import BNSOwnership from './images/bns.jpg';
import BaseNFT from './images/base-nft.svg';
import coinbaseOneVerification from './images/coinbase-one-verification.svg';
import coinbaseVerification from './images/coinbase-verification.svg';
import { StaticImageData } from 'next/dist/shared/lib/get-img-props';
@@ -43,6 +45,12 @@ export default function RegistrationLearnMoreModal({
const SummerPassRowClasses = classNames(rowClasses, {
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.SUMMER_PASS_LVL_3),
});
const BNSRowClasses = classNames(rowClasses, {
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.BNS_NAME),
});
const BaseDotEthNFTRowClasses = classNames(rowClasses, {
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.BASE_ETH_NFT),
});
const qualifiedClasses = classNames(
'flex flex-row items-center justify-center py-3 px-1 h-5 text-xs bg-green-0 rounded ml-3',
@@ -89,9 +97,9 @@ export default function RegistrationLearnMoreModal({
width={30}
height={30}
wrapperClassName="rounded-full"
imageClassName={CBRowClasses}
imageClassName={CB1RowClasses}
/>
<p className={classNames(CB1RowClasses)}>Coinbase One verification</p>
<p className={CB1RowClasses}>Coinbase One verification</p>
<InfoIcon />
</div>
</Tooltip>
@@ -110,9 +118,9 @@ export default function RegistrationLearnMoreModal({
width={30}
height={30}
wrapperClassName="rounded-full"
imageClassName={CBRowClasses}
imageClassName={CBIDRowClasses}
/>
<p className={classNames(CBIDRowClasses)}>A cb.id username</p>
<p className={CBIDRowClasses}>A cb.id username</p>
<InfoIcon />
</div>
</Tooltip>
@@ -131,9 +139,9 @@ export default function RegistrationLearnMoreModal({
width={30}
height={30}
wrapperClassName="rounded-full"
imageClassName={CBRowClasses}
imageClassName={BuildathonRowClasses}
/>
<p className={classNames(BuildathonRowClasses)}>Base buildathon participant</p>
<p className={BuildathonRowClasses}>Base buildathon participant</p>
<InfoIcon />
</div>
</Tooltip>
@@ -152,9 +160,9 @@ export default function RegistrationLearnMoreModal({
width={30}
height={30}
wrapperClassName="rounded-full"
imageClassName={CBRowClasses}
imageClassName={SummerPassRowClasses}
/>
<p className={classNames(SummerPassRowClasses)}>Summer Pass Level 3</p>
<p className={SummerPassRowClasses}>Summer Pass Level 3</p>
<InfoIcon />
</div>
</Tooltip>
@@ -164,6 +172,48 @@ export default function RegistrationLearnMoreModal({
</div>
)}
</li>
<li className="flex items-center gap-3">
<Tooltip content="BNS (.base) username holders are eligible for a 0.01 ETH discount">
<div className="flex flex-row items-center justify-start gap-2">
<ImageWithLoading
src={BNSOwnership}
alt="criteria icon"
width={30}
height={30}
wrapperClassName="rounded-full"
imageClassName={BNSRowClasses}
/>
<p className={BNSRowClasses}>BNS username</p>
<InfoIcon />
</div>
</Tooltip>
{allActiveDiscounts.has(Discount.BNS_NAME) && (
<div className={qualifiedClasses}>
<p className="text-green-60">Qualified</p>
</div>
)}
</li>
<li className="flex items-center gap-3">
<Tooltip content="Available for anyone holding a base.eth NFT">
<div className="flex flex-row items-center justify-start gap-2">
<ImageWithLoading
src={BaseNFT as StaticImageData}
alt="criteria icon"
width={30}
height={30}
wrapperClassName="rounded-full"
imageClassName={BaseDotEthNFTRowClasses}
/>
<p className={BaseDotEthNFTRowClasses}>Base.eth NFT</p>
<InfoIcon />
</div>
</Tooltip>
{allActiveDiscounts.has(Discount.BASE_ETH_NFT) && (
<div className={qualifiedClasses}>
<p className="text-green-60">Qualified</p>
</div>
)}
</li>
</ul>
{!hasDiscount && (
<>

View File

@@ -4,6 +4,10 @@ import {
useCheckCBIDAttestations,
useCheckCoinbaseAttestations,
useCheckEAAttestations,
// useBuildathonAttestations,
// useSummerPassAttestations,
// useBaseDotEthAttestations,
useBNSAttestations,
} from 'apps/web/src/hooks/useAttestations';
import { useActiveDiscountValidators } from 'apps/web/src/hooks/useReadActiveDiscountValidators';
import { Discount } from 'apps/web/src/utils/usernames';
@@ -32,13 +36,21 @@ export function useAggregatedDiscountValidators() {
const { data: EAData, loading: loadingEAAttestations } = useCheckEAAttestations();
const { data: coinbaseData, loading: loadingCoinbaseAttestations } =
useCheckCoinbaseAttestations();
// const { data: BuildathonData, loading: loadingBuildathon } = useBuildathonAttestations();
// const { data: SummerPassData, loading: loadingSummerPass } = useSummerPassAttestations();
// const { data: BaseDotEthData, loading: loadingBaseDotEth } = useBaseDotEthAttestations();
const { data: BNSData, loading: loadingBNS } = useBNSAttestations();
const loadingDiscounts =
loadingCoinbaseAttestations ||
loadingCBIDAttestations ||
loadingCB1Attestations ||
loadingActiveDiscounts ||
loadingEAAttestations;
loadingEAAttestations ||
// loadingBuildathon ||
// loadingSummerPass ||
// loadingBaseDotEth ||
loadingBNS;
const discountsToAttestationData = useMemo<MappedDiscountData>(() => {
const discountMapping: MappedDiscountData = {};
@@ -59,14 +71,51 @@ export function useAggregatedDiscountValidators() {
discountKey: validator.key,
};
}
if (EAData && validator.discountValidator === EAData.discountValidatorAddress) {
discountMapping[Discount.EARLY_ACCESS] = { ...EAData, discountKey: validator.key };
}
// if (
// BuildathonData &&
// validator.discountValidator === BuildathonData.discountValidatorAddress
// ) {
// discountMapping[Discount.BASE_BUILDATHON_PARTICIPANT] = {
// ...BuildathonData,
// discountKey: validator.key,
// };
// }
// if (
// SummerPassData &&
// validator.discountValidator === SummerPassData.discountValidatorAddress
// ) {
// discountMapping[Discount.SUMMER_PASS_LVL_3] = {
// ...SummerPassData,
// discountKey: validator.key,
// };
// }
// if (
// BaseDotEthData &&
// validator.discountValidator === BaseDotEthData.discountValidatorAddress
// ) {
// discountMapping[Discount.BASE_ETH_NFT] = { ...BaseDotEthData, discountKey: validator.key };
// }
if (BNSData && validator.discountValidator === BNSData.discountValidatorAddress) {
discountMapping[Discount.BNS_NAME] = { ...BNSData, discountKey: validator.key };
}
});
return discountMapping;
}, [activeDiscountValidators, CBIDData, CB1Data, coinbaseData, EAData]);
}, [
activeDiscountValidators,
CBIDData,
CB1Data,
coinbaseData,
EAData,
// BuildathonData,
// SummerPassData,
// BaseDotEthData,
BNSData,
]);
return {
data: discountsToAttestationData,

View File

@@ -10,6 +10,7 @@ import { Address, ReadContractErrorType, encodeAbiParameters } from 'viem';
import { useAccount, useReadContract } from 'wagmi';
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
import { useErrors } from 'apps/web/contexts/Errors';
import { BNSProofResponse } from 'apps/web/pages/api/proofs/bns';
export type AttestationData = {
discountValidatorAddress: Address;
@@ -225,17 +226,13 @@ export function useCheckEAAttestations(): AttestationHookReturns {
useEffect(() => {
async function checkEarlyAccess(a: string) {
try {
const params = new URLSearchParams();
params.append('address', a);
params.append('chain', basenameChain.id.toString());
const response = await fetch(`/api/proofs/earlyAccess?${params}`);
if (response.ok) {
const result = (await response.json()) as EarlyAccessProofResponse;
setEAProofResponse(result);
}
} catch (error) {
logError(error, 'Error checking early access');
const params = new URLSearchParams();
params.append('address', a);
params.append('chain', basenameChain.id.toString());
const response = await fetch(`/api/proofs/earlyAccess?${params}`);
if (response.ok) {
const result = (await response.json()) as EarlyAccessProofResponse;
setEAProofResponse(result);
}
}
@@ -281,3 +278,75 @@ export function useCheckEAAttestations(): AttestationHookReturns {
}
return { data: null, loading: isLoading, error };
}
// export function useBuildathonAttestations() {
// return { data: null, loading: isLoading, error };
// }
// export function useSummerPassAttestations() {
// return { data: null, loading: isLoading, error };
// }
// export function useBaseDotEthAttestations() {
// return { data: null, loading: isLoading, error };
// }
// merkle tree discount calls api endpoint
export function useBNSAttestations() {
const { address } = useAccount();
const [proofResponse, setProofResponse] = useState<BNSProofResponse | null>(null);
const { basenameChain } = useBasenameChain();
const { logError } = useErrors();
useEffect(() => {
async function checkBNS(a: string) {
const params = new URLSearchParams();
params.append('address', a);
params.append('chain', basenameChain.id.toString());
const response = await fetch(`/api/proofs/bns?${params}`);
if (response.ok) {
const result = (await response.json()) as BNSProofResponse;
setProofResponse(result);
}
}
if (address) {
checkBNS(address).catch((error) => {
logError(error, 'Error checking BNS discount availability');
});
}
}, [address, basenameChain.id, logError]);
const encodedProof = useMemo(
() =>
proofResponse?.proofs
? encodeAbiParameters([{ type: 'bytes32[]' }], [proofResponse?.proofs])
: '0x0',
[proofResponse?.proofs],
);
const readContractArgs = useMemo(() => {
if (!proofResponse?.proofs || !address) {
return {};
}
return {
address: proofResponse?.discountValidatorAddress,
abi: EarlyAccessValidatorABI,
functionName: 'isValidDiscountRegistration',
args: [address, encodedProof],
};
}, [address, proofResponse?.discountValidatorAddress, proofResponse?.proofs, encodedProof]);
const { data: isValid, isLoading, error } = useReadContract(readContractArgs);
if (isValid && proofResponse && address) {
return {
data: {
discountValidatorAddress: proofResponse.discountValidatorAddress,
discount: Discount.BNS_NAME,
validationData: encodedProof,
},
loading: false,
error: null,
};
}
return { data: null, loading: isLoading, error };
}

View File

@@ -335,6 +335,8 @@ export enum Discount {
COINBASE_VERIFIED_ACCOUNT = 'COINBASE_VERIFIED_ACCOUNT',
BASE_BUILDATHON_PARTICIPANT = 'BASE_BUILDATHON_PARTICIPANT',
SUMMER_PASS_LVL_3 = 'SUMMER_PASS_LVL_3',
BNS_NAME = 'BNS_NAME',
BASE_ETH_NFT = 'BASE_ETH_NFT',
}
export function isValidDiscount(key: string): key is keyof typeof Discount {