Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement Affiliates protocol interactions #1074

Merged
merged 15 commits into from
Oct 3, 2024
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
"@cosmjs/tendermint-rpc": "^0.32.1",
"@datadog/browser-logs": "^5.23.3",
"@dydxprotocol/v4-abacus": "1.12.12",
"@dydxprotocol/v4-client-js": "1.6.1",
"@dydxprotocol/v4-client-js": "1.10.0",
"@dydxprotocol/v4-localization": "^1.1.213",
"@dydxprotocol/v4-proto": "^6.0.1",
"@dydxprotocol/v4-proto": "^7.0.0-dev.0",
"@emotion/is-prop-valid": "^1.3.0",
"@ethersproject/providers": "^5.7.2",
"@hugocxl/react-to-image": "^0.0.9",
Expand Down
18 changes: 6 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/constants/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export enum LocalStorageKey {
EvmDerivedAddresses = 'dydx.EvmDerivedAddresses',
KeplrCompliance = 'dydx.KeplrCompliance',
SolDerivedAddresses = 'dydx.SolDerivedAddresses',
LatestReferrer = 'dydx.LatestReferrer',

// Gas
SelectedGasDenom = 'dydx.SelectedGasDenom',
Expand Down
40 changes: 25 additions & 15 deletions src/hooks/useAffiliatesInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';

import { useAccounts } from './useAccounts';
import { log } from '@/lib/telemetry';

import { useDydxClient } from './useDydxClient';

type AffiliatesMetadata = {
Expand All @@ -9,31 +10,40 @@ 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) {
return undefined;
}
const endpoint = `${compositeClient.indexerClient.config.restEndpoint}/v4/affiliates/metadata`;
const response = await fetch(`${endpoint}?address=${encodeURIComponent(dydxAddress)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

const data = await response.json();
return data as AffiliatesMetadata | undefined;

try {
const response = await fetch(`${endpoint}?address=${encodeURIComponent(dydxAddress)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const affiliateInfo = await getAffiliateInfo(dydxAddress);

const data: AffiliatesMetadata | undefined = await response.json();
const isEligible =
Boolean(data?.isVolumeEligible) || Boolean(affiliateInfo?.isWhitelisted) || true;

return { metadata: data, affiliateInfo, isEligible };
} catch (error) {
log('useAffiliatesInfo', error, { endpoint });
throw error;
}
};

const { data, isFetched } = useQuery({
const query = useQuery({
queryKey: ['affiliatesMetadata', dydxAddress],
queryFn,
enabled: Boolean(compositeClient && dydxAddress),
});

return { data, isFetched };
return query;
};
16 changes: 16 additions & 0 deletions src/hooks/useDydxClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,20 @@ const useDydxClientContext = () => {
[compositeClient]
);

const getAffiliateInfo = useCallback(
async (address: string) => {
return compositeClient?.validatorClient.get.getAffiliateInfo(address);
},
[compositeClient]
);

const getReferredBy = useCallback(
async (address: string) => {
return compositeClient?.validatorClient.get.getReferredBy(address);
},
[compositeClient]
);

return {
// Client initialization
connect: setNetworkConfig,
Expand Down Expand Up @@ -518,6 +532,8 @@ const useDydxClientContext = () => {
getWithdrawalCapacityByDenom,
getValidators,
getAccountBalance,
getAffiliateInfo,
getReferredBy,

// vault methods
getMegavaultHistoricalPnl,
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useReferralAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export const useReferralAddress = (refCode: string) => {
return data?.address as string | undefined;
};

const { data, isFetched } = useQuery({
const query = useQuery({
queryKey: ['referralAddress', refCode],
queryFn,
enabled: Boolean(compositeClient && refCode),
});

return { data, isFetched };
return query;
};
36 changes: 36 additions & 0 deletions src/hooks/useReferredBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useQuery } from '@tanstack/react-query';

import { DydxAddress } from '@/constants/wallets';

import { log } from '@/lib/telemetry';

import { useAccounts } from './useAccounts';
import { useDydxClient } from './useDydxClient';

export const useReferredBy = () => {
const { dydxAddress } = useAccounts();
const { getReferredBy, compositeClient } = useDydxClient();

const queryFn = async ({ queryKey }: { queryKey: (string | DydxAddress | undefined)[] }) => {
const [, address] = queryKey;
if (!address) {
return undefined;
}
try {
const affliateAddress = await getReferredBy(address);

return affliateAddress?.affiliateAddress;
} catch (error) {
log('useReferredBy', error);
return undefined;
}
};

const { data, isFetched } = useQuery({
queryKey: ['referredBy', dydxAddress],
queryFn,
enabled: Boolean(compositeClient && dydxAddress),
});

return { data, isFetched };
};
36 changes: 36 additions & 0 deletions src/hooks/useRegisterAffiliate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useQuery } from '@tanstack/react-query';

import { DydxAddress } from '@/constants/wallets';

import { log } from '@/lib/telemetry';

import { useAccounts } from './useAccounts';
import { useDydxClient } from './useDydxClient';

export const useReferredBy = () => {
const { dydxAddress } = useAccounts();
const { getReferredBy } = useDydxClient();

const queryFn = async ({ queryKey }: { queryKey: (string | DydxAddress | undefined)[] }) => {
const [, address] = queryKey;
if (!address) {
return undefined;
}
try {
const affliateAddress = await getReferredBy(address);

return affliateAddress?.affiliateAddress;
} catch (error) {
log('useReferredBy', error);
return undefined;
}
};

const { data, isFetched } = useQuery({
queryKey: ['referredBy', dydxAddress],
queryFn,
enabled: Boolean(dydxAddress),
});

return { data, isFetched };
};
81 changes: 80 additions & 1 deletion src/hooks/useSubaccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type GovAddNewMarketParams,
type LocalWallet,
} from '@dydxprotocol/v4-client-js';
import { useMutation } from '@tanstack/react-query';
import Long from 'long';
import { shallowEqual } from 'react-redux';
import { formatUnits, parseUnits } from 'viem';
Expand All @@ -32,6 +33,8 @@ import { DydxAddress, WalletType } from '@/constants/wallets';

import { clearSubaccountState } from '@/state/account';
import { getBalances, getSubaccountOrders } from '@/state/accountSelectors';
import { removeLatestReferrer } from '@/state/affiliates';
import { getLatestReferrer } from '@/state/affiliatesSelector';
import { useAppDispatch, useAppSelector } from '@/state/appTypes';
import { openDialog } from '@/state/dialogs';
import {
Expand All @@ -55,6 +58,7 @@ import { hashFromTx } from '@/lib/txUtils';
import { useAccounts } from './useAccounts';
import { useDydxClient } from './useDydxClient';
import { useGovernanceVariables } from './useGovernanceVariables';
import { useReferredBy } from './useReferredBy';
import { useTokenConfigs } from './useTokenConfigs';

type SubaccountContextType = ReturnType<typeof useSubaccountContext>;
Expand Down Expand Up @@ -286,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,
Expand Down Expand Up @@ -909,6 +912,79 @@ 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 can not be the same as referree');
}
try {
const response = await compositeClient?.validatorClient.post.registerAffiliate(
subaccountClient,
affiliate
);
return response;
} catch (error) {
log('useSubaccount/registerAffiliate', error);
throw error;
}
},
[subaccountClient, compositeClient]
);

const latestReferrer = useAppSelector(getLatestReferrer);
const { data: referredBy, isFetched: isReferredByFetched } = useReferredBy();

const { mutateAsync: registerAffiliateMutate, isPending: isRegisterAffiliatePending } =
useMutation({
rosepuppy marked this conversation as resolved.
Show resolved Hide resolved
mutationFn: async (affiliate: string) => {
const tx = await registerAffiliate(affiliate);
dispatch(removeLatestReferrer());
return tx;
},
});

useEffect(() => {
if (!subaccountClient) return;

if (dydxAddress === latestReferrer) {
dispatch(removeLatestReferrer());
return;
}
if (
rosepuppy marked this conversation as resolved.
Show resolved Hide resolved
latestReferrer &&
dydxAddress &&
usdcCoinBalance &&
parseFloat(usdcCoinBalance.amount) > AMOUNT_USDC_BEFORE_REBALANCE &&
isReferredByFetched &&
!referredBy &&
!isRegisterAffiliatePending
) {
registerAffiliateMutate(latestReferrer);
}
}, [
latestReferrer,
dydxAddress,
registerAffiliateMutate,
usdcCoinBalance,
subaccountClient,
isReferredByFetched,
referredBy,
dispatch,
isRegisterAffiliatePending,
]);

useEffect(() => {
if (referredBy && latestReferrer) {
dispatch(removeLatestReferrer());
}
}, [referredBy, dispatch, latestReferrer]);

const getVaultAccountInfo = useCallback(async () => {
if (!compositeClient?.validatorClient) {
throw new Error('client not initialized');
Expand Down Expand Up @@ -983,6 +1059,9 @@ const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: LocalWall
withdrawReward,
getWithdrawRewardFee,

// affiliates
registerAffiliate,

// vaults
getVaultAccountInfo,
depositToMegavault,
Expand Down
4 changes: 3 additions & 1 deletion src/state/_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import storage from 'redux-persist/lib/storage';
import abacusStateManager from '@/lib/abacus';

import { accountSlice } from './account';
import { affiliatesSlice } from './affiliates';
import { appSlice } from './app';
import appMiddleware from './appMiddleware';
import { assetsSlice } from './assets';
Expand All @@ -24,6 +25,7 @@ import { walletSlice } from './wallet';

const reducers = {
account: accountSlice.reducer,
affiliates: affiliatesSlice.reducer,
app: appSlice.reducer,
assets: assetsSlice.reducer,
configs: configsSlice.reducer,
Expand All @@ -45,7 +47,7 @@ const persistConfig = {
key: 'root',
version: 1,
storage,
whitelist: ['tradingView', 'wallet'],
whitelist: ['tradingView', 'wallet', 'affiliates'],
migrate: customCreateMigrate({ debug: process.env.NODE_ENV !== 'production' }),
};

Expand Down
Loading
Loading