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
6 changes: 3 additions & 3 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-localization": "^1.1.210",
"@dydxprotocol/v4-proto": "^6.0.1",
"@dydxprotocol/v4-client-js": "1.10.0",
"@dydxprotocol/v4-localization": "1.1.210",
"@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
20 changes: 7 additions & 13 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
35 changes: 22 additions & 13 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,24 +10,32 @@ 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);

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

const { data, isFetched } = useQuery({
Expand Down
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
25 changes: 16 additions & 9 deletions src/hooks/useReferralAddress.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useQuery } from '@tanstack/react-query';

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

import { useDydxClient } from './useDydxClient';

export const useReferralAddress = (refCode: string) => {
Expand All @@ -9,16 +11,21 @@ export const useReferralAddress = (refCode: string) => {
if (!compositeClient || !refCode) {
return undefined;
}
const endpoint = `${compositeClient.indexerClient.config.restEndpoint}/v4/affiliates/address`;
const response = await fetch(`${endpoint}?referralCode=${encodeURIComponent(refCode)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
try {
const endpoint = `${compositeClient.indexerClient.config.restEndpoint}/v4/affiliates/address`;
rosepuppy marked this conversation as resolved.
Show resolved Hide resolved
const response = await fetch(`${endpoint}?referralCode=${encodeURIComponent(refCode)}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

const data = await response.json();
return data?.address as string | undefined;
const data = await response.json();
return data?.address as string | undefined;
} catch (error) {
log('useReferralAddress', error);
return undefined;
}
};

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

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 () => {
rosepuppy marked this conversation as resolved.
Show resolved Hide resolved
if (!dydxAddress) {
return undefined;
}
try {
const affliateAddress = await getReferredBy(dydxAddress);

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

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

return { data, isFetched };
};
70 changes: 69 additions & 1 deletion src/hooks/useSubaccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 +57,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 +289,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 +911,69 @@ 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);
return undefined;
}
},
[subaccountClient, compositeClient]
);

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

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
) {
registerAffiliate(latestReferrer);
dispatch(removeLatestReferrer());
rosepuppy marked this conversation as resolved.
Show resolved Hide resolved
}
}, [
latestReferrer,
dydxAddress,
registerAffiliate,
usdcCoinBalance,
subaccountClient,
isReferredByFetched,
referredBy,
dispatch,
]);

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

const getVaultAccountInfo = useCallback(async () => {
if (!compositeClient?.validatorClient) {
throw new Error('client not initialized');
Expand Down Expand Up @@ -983,6 +1048,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 @@ -23,6 +24,7 @@ import { vaultsSlice } from './vaults';

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

Expand Down
25 changes: 25 additions & 0 deletions src/state/affiliates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';

export interface AffiliatesState {
latestReferrer: string | undefined;
}

const initialState: AffiliatesState = {
latestReferrer: undefined,
};

export const affiliatesSlice = createSlice({
name: 'Affiliates',
initialState,
reducers: {
updateLatestReferrer: (state, action: PayloadAction<string>) => {
state.latestReferrer = action.payload;
},
removeLatestReferrer: (state) => {
state.latestReferrer = undefined;
},
},
});

export const { updateLatestReferrer, removeLatestReferrer } = affiliatesSlice.actions;
6 changes: 6 additions & 0 deletions src/state/affiliatesSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { RootState } from './_store';

/**
* @returns saved latestReferrer for Affiliates
*/
export const getLatestReferrer = (state: RootState) => state.affiliates.latestReferrer;
Loading
Loading