From 904f2d196374f181d7d4ad2aea2ea8b9684c69ef Mon Sep 17 00:00:00 2001 From: Bill He Date: Thu, 26 Sep 2024 21:23:38 -0700 Subject: [PATCH 01/10] feat: register affiliates --- src/hooks/useAffiliatesInfo.ts | 13 ++--- src/hooks/useDydxClient.tsx | 8 +++ src/hooks/useReferredBy.ts | 26 ++++++++++ src/hooks/useSubaccount.tsx | 59 +++++++++++++++++++++- src/views/dialogs/ReferralDialog.tsx | 19 ++++--- src/views/dialogs/ShareAffiliateDialog.tsx | 16 +++--- 6 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useReferredBy.ts diff --git a/src/hooks/useAffiliatesInfo.ts b/src/hooks/useAffiliatesInfo.ts index 8888ac7db..c30f7a139 100644 --- a/src/hooks/useAffiliatesInfo.ts +++ b/src/hooks/useAffiliatesInfo.ts @@ -1,6 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { useAccounts } from './useAccounts'; import { useDydxClient } from './useDydxClient'; type AffiliatesMetadata = { @@ -9,9 +8,8 @@ type AffiliatesMetadata = { isAffiliate: boolean; }; -export const useAffiliatesInfo = () => { - const { dydxAddress } = useAccounts(); - const { compositeClient } = useDydxClient(); +export const useAffiliatesInfo = (dydxAddress?: string) => { + const { compositeClient, getAffiliateInfo } = useDydxClient(); const queryFn = async () => { if (!compositeClient || !dydxAddress) { @@ -24,9 +22,12 @@ export const useAffiliatesInfo = () => { 'Content-Type': 'application/json', }, }); + const affiliateInfo = await getAffiliateInfo(dydxAddress); - const data = await response.json(); - return data as AffiliatesMetadata | undefined; + const data: AffiliatesMetadata | undefined = await response.json(); + const isEligible = Boolean(data?.isVolumeEligible) || Boolean(affiliateInfo?.isWhitelisted); + + return { metadata: data, affiliateInfo, isEligible }; }; const { data, isFetched } = useQuery({ diff --git a/src/hooks/useDydxClient.tsx b/src/hooks/useDydxClient.tsx index d0c33d711..6603b950a 100644 --- a/src/hooks/useDydxClient.tsx +++ b/src/hooks/useDydxClient.tsx @@ -420,6 +420,13 @@ const useDydxClientContext = () => { [compositeClient] ); + const getAffiliateInfo = useCallback( + async (address: string) => { + return compositeClient?.validatorClient.get.getAffiliateInfo(address); + }, + [compositeClient] + ); + return { // Client initialization connect: setNetworkConfig, @@ -450,5 +457,6 @@ const useDydxClientContext = () => { getWithdrawalCapacityByDenom, getValidators, getAccountBalance, + getAffiliateInfo, }; }; diff --git a/src/hooks/useReferredBy.ts b/src/hooks/useReferredBy.ts new file mode 100644 index 000000000..7d226042f --- /dev/null +++ b/src/hooks/useReferredBy.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useAccounts } from './useAccounts'; +import { useSubaccount } from './useSubaccount'; + +export const useReferralAddress = () => { + const { dydxAddress } = useAccounts(); + const { getReferredBy } = useSubaccount(); + + const queryFn = async () => { + if (!dydxAddress) { + return undefined; + } + const affliateAddress = await getReferredBy(dydxAddress); + + return affliateAddress; + }; + + const { data, isFetched } = useQuery({ + queryKey: ['referredBy', dydxAddress], + queryFn, + enabled: Boolean(dydxAddress), + }); + + return { data, isFetched }; +}; diff --git a/src/hooks/useSubaccount.tsx b/src/hooks/useSubaccount.tsx index 884d2075a2..ccfe5c333 100644 --- a/src/hooks/useSubaccount.tsx +++ b/src/hooks/useSubaccount.tsx @@ -26,6 +26,7 @@ import { AMOUNT_RESERVED_FOR_GAS_USDC, AMOUNT_USDC_BEFORE_REBALANCE } from '@/co import { DEFAULT_TRANSACTION_MEMO, TransactionMemo } from '@/constants/analytics'; import { DialogTypes } from '@/constants/dialogs'; import { ErrorParams } from '@/constants/errors'; +import { LocalStorageKey } from '@/constants/localStorage'; import { QUANTUM_MULTIPLIER } from '@/constants/numbers'; import { TradeTypes } from '@/constants/trade'; import { DydxAddress, WalletType } from '@/constants/wallets'; @@ -53,6 +54,7 @@ import { hashFromTx } from '@/lib/txUtils'; import { useAccounts } from './useAccounts'; import { useDydxClient } from './useDydxClient'; import { useGovernanceVariables } from './useGovernanceVariables'; +import { useLocalStorage } from './useLocalStorage'; import { useTokenConfigs } from './useTokenConfigs'; type SubaccountContextType = ReturnType; @@ -75,6 +77,10 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall const { connectedWallet } = useAccounts(); const { compositeClient, faucetClient } = useDydxClient(); + const [latestReferrer] = useLocalStorage({ + key: LocalStorageKey.LatestReferrer, + defaultValue: undefined, + }); const isKeplr = connectedWallet?.name === WalletType.Keplr; const { getFaucetFunds, getNativeTokens } = useMemo( @@ -284,7 +290,6 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall const balanceAmount = parseFloat(balance.amount); const shouldDeposit = balanceAmount - AMOUNT_RESERVED_FOR_GAS_USDC > 0; const shouldWithdraw = balanceAmount - AMOUNT_USDC_BEFORE_REBALANCE <= 0; - if (shouldDeposit) { await depositToSubaccount({ amount: balanceAmount - AMOUNT_RESERVED_FOR_GAS_USDC, @@ -877,6 +882,54 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall [localDydxWallet, compositeClient] ); + const registerAffiliate = useCallback( + async (affiliate: string) => { + if (!compositeClient) { + throw new Error('client not initialized'); + } + if (!subaccountClient?.wallet?.address) { + throw new Error('wallet not initialized'); + } + if (affiliate === subaccountClient?.wallet?.address) { + throw new Error('affiliate not be the same as referree'); + } + + const response = await compositeClient?.validatorClient.post.registerAffiliate( + subaccountClient, + affiliate + ); + + return response; + }, + [subaccountClient, compositeClient] + ); + + const getReferredBy = useCallback( + async (address: string) => { + if (!compositeClient) { + throw new Error('client not initialized'); + } + + const response = await compositeClient?.validatorClient.get.getReferredBy(address); + + return response; + }, + [compositeClient] + ); + + useEffect(() => { + if (!subaccountClient) return; + + if ( + latestReferrer && + dydxAddress && + usdcCoinBalance && + parseFloat(usdcCoinBalance.amount) > AMOUNT_USDC_BEFORE_REBALANCE + ) { + registerAffiliate(latestReferrer); + } + }, [latestReferrer, dydxAddress, registerAffiliate, usdcCoinBalance, subaccountClient]); + return { // Deposit/Withdraw/Faucet Methods deposit, @@ -906,5 +959,9 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall getUndelegateFee, withdrawReward, getWithdrawRewardFee, + + // affiliates + registerAffiliate, + getReferredBy, }; }; diff --git a/src/views/dialogs/ReferralDialog.tsx b/src/views/dialogs/ReferralDialog.tsx index 946c782d6..2abe0abbb 100644 --- a/src/views/dialogs/ReferralDialog.tsx +++ b/src/views/dialogs/ReferralDialog.tsx @@ -9,6 +9,7 @@ import { LocalStorageKey } from '@/constants/localStorage'; import { STRING_KEYS } from '@/constants/localization'; import { useAccounts } from '@/hooks/useAccounts'; +import { useAffiliatesInfo } from '@/hooks/useAffiliatesInfo'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useReferralAddress } from '@/hooks/useReferralAddress'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -39,16 +40,20 @@ export const ReferralDialog = ({ setIsOpen, refCode }: DialogProps({ key: LocalStorageKey.LatestReferrer, defaultValue: undefined, }); useEffect(() => { - if (referralAddress) { + if (referralAddress && referralAddress !== dydxAddress) { saveLastestReferrer(referralAddress); } - }, [referralAddress, saveLastestReferrer]); + }, [dydxAddress, referralAddress, saveLastestReferrer]); + + const isEligible = referralAddress && affiliatesInfo?.isEligible; return ( {stringGetter({ - key: referralAddress ? STRING_KEYS.YOUR_FRIEND : STRING_KEYS.WELCOME_DYDX, + key: isEligible ? STRING_KEYS.YOUR_FRIEND : STRING_KEYS.WELCOME_DYDX, })} {truncateAddress(referralAddress)} } description={stringGetter({ - key: referralAddress ? STRING_KEYS.INVITED_YOU : STRING_KEYS.THE_PRO_TRADING_PLATFORM, + key: isEligible ? STRING_KEYS.INVITED_YOU : STRING_KEYS.THE_PRO_TRADING_PLATFORM, })} slotHeaderAbove={ - isFetched ? ( + isFetched && isAffiliatesInfoFetched ? ( <$HeaderAbove tw="flex flex-row items-center gap-1"> - {referralAddress ? ( + {isEligible ? ( hedgie ) : (
@@ -81,7 +86,7 @@ export const ReferralDialog = ({ setIsOpen, refCode }: DialogProps <$Triangle />
- {referralAddress ? ( + {isEligible ? ( {stringGetter({ key: STRING_KEYS.REFER_FOR_DISCOUNTS_FIRST_ORDER, diff --git a/src/views/dialogs/ShareAffiliateDialog.tsx b/src/views/dialogs/ShareAffiliateDialog.tsx index 28a6e1494..9c3610613 100644 --- a/src/views/dialogs/ShareAffiliateDialog.tsx +++ b/src/views/dialogs/ShareAffiliateDialog.tsx @@ -7,6 +7,7 @@ import { DialogProps, ShareAffiliateDialogProps } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { ColorToken } from '@/constants/styles/base'; +import { useAccounts } from '@/hooks/useAccounts'; import { useAffiliatesInfo } from '@/hooks/useAffiliatesInfo'; import { useStringGetter } from '@/hooks/useStringGetter'; import { useURLConfigs } from '@/hooks/useURLConfigs'; @@ -32,7 +33,8 @@ const copyBlobToClipboard = async (blob: Blob | null) => { export const ShareAffiliateDialog = ({ setIsOpen }: DialogProps) => { const stringGetter = useStringGetter(); const { affiliateProgram } = useURLConfigs(); - const { data } = useAffiliatesInfo(); + const { dydxAddress } = useAccounts(); + const { data } = useAffiliatesInfo(dydxAddress as string); const [{ isLoading: isCopying }, , ref] = useToBlob({ quality: 1.0, @@ -56,7 +58,7 @@ export const ShareAffiliateDialog = ({ setIsOpen }: DialogProps
- {data?.isVolumeEligible + {data?.isEligible ? stringGetter({ key: STRING_KEYS.AFFILIATE_LINK }) : stringGetter({ key: STRING_KEYS.AFFILIATE_LINK_REQUIREMENT, @@ -92,7 +94,7 @@ export const ShareAffiliateDialog = ({ setIsOpen }: DialogProps
- {data?.isVolumeEligible + {data?.isEligible ? affiliatesUrl : stringGetter({ key: STRING_KEYS.YOUVE_TRADED, @@ -103,7 +105,7 @@ export const ShareAffiliateDialog = ({ setIsOpen }: DialogProps
- {data?.isVolumeEligible && ( + {data?.isEligible && ( {stringGetter({ key: STRING_KEYS.COPY_LINK })} @@ -144,7 +146,7 @@ export const ShareAffiliateDialog = ({ setIsOpen }: DialogProps - {data?.isVolumeEligible && ( + {data?.isEligible && (