Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4a2e7d604 | ||
|
|
6c7d4808e1 | ||
|
|
3c931e180e | ||
|
|
d760e4cc8e | ||
|
|
6f59083169 | ||
|
|
f0b953fb71 | ||
|
|
d46af0009a | ||
|
|
cda3a5050e | ||
|
|
76048fea66 | ||
|
|
5bbb592b01 | ||
|
|
722897fdc6 | ||
|
|
961c6e2ee6 | ||
|
|
9e413e66ae | ||
|
|
c46d701361 |
@@ -290,5 +290,5 @@ jobs:
|
||||
if [[ $result == "success" || $result == "skipped" ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 0
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -93,7 +93,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
.and('contain.text', 'consensus nodes');
|
||||
});
|
||||
|
||||
it.skip('should contain link to specific validators', function () {
|
||||
it('should contain link to specific validators', function () {
|
||||
cy.getByTestId('validators')
|
||||
.should('have.length', '2')
|
||||
.each(($validator) => {
|
||||
|
||||
@@ -26,9 +26,9 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
const { token, staking, vesting } = useContracts();
|
||||
const setAssociatedBalances = useRefreshAssociatedBalances();
|
||||
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
|
||||
const vegaConnecting = useEagerConnect();
|
||||
const vegaWalletStatus = useEagerConnect();
|
||||
|
||||
const loaded = balancesLoaded && !vegaConnecting;
|
||||
const loaded = balancesLoaded && vegaWalletStatus !== 'connecting';
|
||||
|
||||
React.useEffect(() => {
|
||||
const run = async () => {
|
||||
@@ -169,3 +169,5 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
}
|
||||
return <Suspense fallback={loading}>{children}</Suspense>;
|
||||
};
|
||||
|
||||
AppLoader.displayName = 'AppLoader';
|
||||
|
||||
@@ -111,3 +111,4 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
|
||||
</ContractsContext.Provider>
|
||||
);
|
||||
};
|
||||
ContractsProvider.displayName = 'ContractsProvider';
|
||||
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
useVegaWallet,
|
||||
useDialogStore,
|
||||
} from '@vegaprotocol/wallet-react';
|
||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { Statistics, useStats } from './referral-statistics';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { ns, useT } from '../../lib/use-t';
|
||||
import { useFundsAvailable } from './hooks/use-funds-available';
|
||||
@@ -24,12 +26,6 @@ import { ViewType, useSidebar } from '../../components/sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { QUSDTooltip } from './qusd-tooltip';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { PreviewRefereeStatistics } from './referee-statistics';
|
||||
import {
|
||||
useReferralSet,
|
||||
useIsInReferralSet,
|
||||
} from './hooks/use-find-referral-set';
|
||||
import minBy from 'lodash/minBy';
|
||||
|
||||
const RELOAD_DELAY = 3000;
|
||||
|
||||
@@ -110,11 +106,9 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
|
||||
const codeField = watch('code');
|
||||
|
||||
const {
|
||||
data: previewData,
|
||||
loading: previewLoading,
|
||||
isEligible: isPreviewEligible,
|
||||
} = useReferralSet(validateCode(codeField, t) ? codeField : undefined);
|
||||
const { data: previewData, loading: previewLoading } = useReferral({
|
||||
code: validateCode(codeField, t) ? codeField : undefined,
|
||||
});
|
||||
|
||||
const { send, status } = useSimpleTransaction({
|
||||
onSuccess: () => {
|
||||
@@ -147,14 +141,19 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
* Validates the set a user tries to apply to.
|
||||
*/
|
||||
const validateSet = useCallback(() => {
|
||||
if (codeField && !previewLoading && previewData && !isPreviewEligible) {
|
||||
if (
|
||||
codeField &&
|
||||
!previewLoading &&
|
||||
previewData &&
|
||||
!previewData.isEligible
|
||||
) {
|
||||
return t('The code is no longer valid.');
|
||||
}
|
||||
if (codeField && !previewLoading && !previewData) {
|
||||
return t('The code is invalid');
|
||||
}
|
||||
return true;
|
||||
}, [codeField, isPreviewEligible, previewData, previewLoading, t]);
|
||||
}, [codeField, previewData, previewLoading, t]);
|
||||
|
||||
const noFunds = validateFundsAvailable() !== true ? true : false;
|
||||
|
||||
@@ -201,6 +200,8 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
// });
|
||||
};
|
||||
|
||||
const { epochsValue, nextBenefitTierValue } = useStats({ program });
|
||||
|
||||
// show "code applied" message when successfully applied
|
||||
if (status === 'confirmed') {
|
||||
return (
|
||||
@@ -263,10 +264,9 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
};
|
||||
};
|
||||
|
||||
// calculate minimum amount of epochs a referee has to be in a set in order
|
||||
// to benefit from it
|
||||
const firstBenefitTier = minBy(program.benefitTiers, (bt) => bt.epochs);
|
||||
const minEpochs = firstBenefitTier ? firstBenefitTier.epochs : 0;
|
||||
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
||||
? nextBenefitTierValue.epochs - epochsValue
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -335,17 +335,17 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{previewData && isPreviewEligible ? (
|
||||
{/* TODO: Re-check plural forms once i18n is updated */}
|
||||
{previewData && previewData.isEligible ? (
|
||||
<div className="mt-10">
|
||||
<h2 className="mb-5 text-2xl">
|
||||
{t(
|
||||
'youAreJoiningTheGroup',
|
||||
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
|
||||
{ count: minEpochs }
|
||||
{ count: nextBenefitTierEpochsValue }
|
||||
)}
|
||||
</h2>
|
||||
<PreviewRefereeStatistics setId={codeField} />
|
||||
<Statistics data={previewData} program={program} as="referee" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
import { getUserLocale } from '@vegaprotocol/utils';
|
||||
|
||||
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||
export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
@@ -11,19 +8,3 @@ export const REFERRAL_DOCS_LINK =
|
||||
export const ABOUT_REFERRAL_DOCS_LINK =
|
||||
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
||||
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
export type StatValue<T> = {
|
||||
value: T;
|
||||
loading: boolean;
|
||||
error?: ApolloError | Error;
|
||||
};
|
||||
|
||||
export const COMPACT_NUMBER_FORMAT = (maximumFractionDigits = 2) =>
|
||||
new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
@@ -16,16 +16,13 @@ import {
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../lib/links';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||
import { Trans } from 'react-i18next';
|
||||
import {
|
||||
useFindReferralSet,
|
||||
useIsInReferralSet,
|
||||
} from './hooks/use-find-referral-set';
|
||||
|
||||
export const CreateCodeContainer = () => {
|
||||
const t = useT();
|
||||
@@ -148,7 +145,7 @@ const CreateCodeDialog = ({
|
||||
const t = useT();
|
||||
const createLink = useLinks(DApp.Governance);
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { refetch } = useFindReferralSet(pubKey);
|
||||
const { refetch } = useReferral({ pubKey, role: 'referrer' });
|
||||
const {
|
||||
err,
|
||||
code,
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
type ReferralSetsQueryVariables,
|
||||
useReferralSetsQuery,
|
||||
} from './__generated__/ReferralSets';
|
||||
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
|
||||
|
||||
export type Role = 'referrer' | 'referee';
|
||||
type Args = (
|
||||
| { setId: string | undefined }
|
||||
| { pubKey: string | undefined; role: Role }
|
||||
) & {
|
||||
aggregationEpochs?: number;
|
||||
};
|
||||
|
||||
export const prepareVariables = (
|
||||
args: Args
|
||||
): [ReferralSetsQueryVariables, boolean] => {
|
||||
const byId = 'setId' in args;
|
||||
const byRole = 'pubKey' in args && 'role' in args;
|
||||
let variables = {};
|
||||
let skip = true;
|
||||
if (byId) {
|
||||
variables = {
|
||||
id: args.setId,
|
||||
};
|
||||
skip = !args.setId;
|
||||
}
|
||||
if (byRole) {
|
||||
if (args.role === 'referee') {
|
||||
variables = { referee: args.pubKey };
|
||||
}
|
||||
if (args.role === 'referrer') {
|
||||
variables = { referrer: args.pubKey };
|
||||
}
|
||||
skip = !args.pubKey;
|
||||
}
|
||||
|
||||
return [variables, skip];
|
||||
};
|
||||
|
||||
export const useFindReferralSet = (pubKey?: string) => {
|
||||
const [referrerVariables, referrerSkip] = prepareVariables({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
const [refereeVariables, refereeSkip] = prepareVariables({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
});
|
||||
|
||||
const {
|
||||
data: referrerData,
|
||||
loading: referrerLoading,
|
||||
error: referrerError,
|
||||
refetch: referrerRefetch,
|
||||
} = useReferralSetsQuery({
|
||||
variables: referrerVariables,
|
||||
skip: referrerSkip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
const {
|
||||
data: refereeData,
|
||||
loading: refereeLoading,
|
||||
error: refereeError,
|
||||
refetch: refereeRefetch,
|
||||
} = useReferralSetsQuery({
|
||||
variables: refereeVariables,
|
||||
skip: refereeSkip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const set =
|
||||
referrerData?.referralSets.edges[0]?.node ||
|
||||
refereeData?.referralSets.edges[0]?.node;
|
||||
const role: Role | undefined = set
|
||||
? set?.referrer === pubKey
|
||||
? 'referrer'
|
||||
: 'referee'
|
||||
: undefined;
|
||||
|
||||
const { isEligible } = useStakeAvailable(set?.referrer);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
referrerRefetch();
|
||||
refereeRefetch();
|
||||
}, [refereeRefetch, referrerRefetch]);
|
||||
|
||||
return {
|
||||
data: set,
|
||||
role,
|
||||
loading: referrerLoading || refereeLoading,
|
||||
error: referrerError || refereeError,
|
||||
refetch,
|
||||
isEligible: set ? isEligible : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const useReferralSet = (setId?: string) => {
|
||||
const [variables, skip] = prepareVariables({ setId });
|
||||
const { data, loading, error, refetch } = useReferralSetsQuery({
|
||||
variables,
|
||||
skip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const set = data?.referralSets.edges[0]?.node;
|
||||
const { isEligible } = useStakeAvailable(set?.referrer);
|
||||
|
||||
return {
|
||||
data: set,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
isEligible: set ? isEligible : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const useIsInReferralSet = (pubKey: string | undefined) => {
|
||||
const { data } = useFindReferralSet(pubKey);
|
||||
return Boolean(data);
|
||||
};
|
||||
@@ -1,117 +0,0 @@
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
|
||||
import { findReferee, useReferees } from './use-referees';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type BenefitTier, useReferralProgram } from './use-referral-program';
|
||||
import { type StatValue } from '../constants';
|
||||
import minBy from 'lodash/minBy';
|
||||
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
||||
|
||||
export type RefereeStats = {
|
||||
/** the discount factor -> `discountFactor` ~ `referralDiscountFactor` */
|
||||
discountFactor: StatValue<BigNumber>;
|
||||
/** the benefit tier matching the referee's discount factor */
|
||||
benefitTier: StatValue<BenefitTier | undefined>;
|
||||
/** the next benefit tier after the current referee's tier */
|
||||
nextBenefitTier: StatValue<BenefitTier | undefined>;
|
||||
/** the running volume */
|
||||
runningVolume: StatValue<BigNumber>;
|
||||
/** the number of epochs in set */
|
||||
epochs: StatValue<BigNumber>;
|
||||
};
|
||||
|
||||
const ZERO = BigNumber(0);
|
||||
|
||||
export const useRefereeStats = (
|
||||
pubKey: string,
|
||||
setId: string,
|
||||
aggregationEpochs: number
|
||||
): RefereeStats => {
|
||||
const { data, loading, error } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: setId,
|
||||
},
|
||||
skip: !setId || setId.length === 0 || !pubKey || pubKey.length === 0,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const {
|
||||
benefitTiers,
|
||||
loading: programLoading,
|
||||
error: programError,
|
||||
} = useReferralProgram();
|
||||
|
||||
const {
|
||||
data: epochData,
|
||||
loading: epochsLoading,
|
||||
error: epochsError,
|
||||
} = useEpochInfoQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
} = useReferees(setId, aggregationEpochs);
|
||||
|
||||
const referee = findReferee(pubKey, refereesData);
|
||||
const stats = removePaginationWrapper(data?.referralSetStats.edges).find(
|
||||
(s) => s.partyId === pubKey
|
||||
);
|
||||
|
||||
const discountFactor = {
|
||||
value: stats?.discountFactor ? BigNumber(stats.discountFactor) : ZERO,
|
||||
loading: loading || refereesLoading,
|
||||
error: error || refereesError,
|
||||
};
|
||||
|
||||
const benefitTier = {
|
||||
value: benefitTiers.find(
|
||||
(t) =>
|
||||
!discountFactor.value.isNaN() &&
|
||||
!isNaN(t.discountFactor) &&
|
||||
t.discountFactor === discountFactor.value.toNumber()
|
||||
),
|
||||
loading: programLoading || discountFactor.loading,
|
||||
error: programError || discountFactor.error,
|
||||
};
|
||||
|
||||
const nextTier = benefitTier.value?.tier
|
||||
? benefitTier.value.tier + 1
|
||||
: undefined;
|
||||
const nextBenefitTier = {
|
||||
value: nextTier
|
||||
? benefitTiers.find((t) => t.tier === nextTier)
|
||||
: minBy(benefitTiers, (t) => t.tier), // min tier number is lowest tier
|
||||
loading: benefitTier.loading,
|
||||
error: benefitTier.error,
|
||||
};
|
||||
|
||||
const runningVolume = {
|
||||
value: stats?.referralSetRunningNotionalTakerVolume
|
||||
? BigNumber(stats.referralSetRunningNotionalTakerVolume)
|
||||
: ZERO,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
const joinedAtEpoch = BigNumber(referee?.atEpoch || '');
|
||||
const currentEpoch = BigNumber(epochData?.epoch.id || '');
|
||||
const epochs = {
|
||||
value:
|
||||
!currentEpoch.isNaN() && !joinedAtEpoch.isNaN()
|
||||
? currentEpoch.minus(joinedAtEpoch)
|
||||
: ZERO,
|
||||
loading: refereesLoading || epochsLoading,
|
||||
error: refereesError || epochsError,
|
||||
};
|
||||
|
||||
return {
|
||||
discountFactor,
|
||||
benefitTier,
|
||||
nextBenefitTier,
|
||||
runningVolume,
|
||||
epochs,
|
||||
};
|
||||
};
|
||||
@@ -1,107 +0,0 @@
|
||||
import { type RefereesQuery } from './__generated__/Referees';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useRefereesQuery } from './__generated__/Referees';
|
||||
import { useCallback } from 'react';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
export type Referee = Omit<
|
||||
NonNullable<RefereesQuery['referralSetReferees']['edges'][0]>['node'],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
/** The properties that can be overwritten by `propertiesOptions`. */
|
||||
type RefereeProperty = keyof Pick<
|
||||
Referee,
|
||||
'totalRefereeGeneratedRewards' | 'totalRefereeNotionalTakerVolume'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Options determining which properties should be overwritten based
|
||||
* on the different `aggregationEpochs`.
|
||||
*/
|
||||
export type PropertiesWithDifferentAggregationEpochs = {
|
||||
properties: RefereeProperty[];
|
||||
aggregationEpochs: number;
|
||||
};
|
||||
|
||||
/** Find referee by its public key (id) */
|
||||
export const findReferee = (pubKey: string, referees: Referee[]) =>
|
||||
referees.find((r) => r.refereeId === pubKey);
|
||||
|
||||
export const useReferees = (
|
||||
id: string | undefined | null,
|
||||
aggregationEpochs: number,
|
||||
propertiesOptions?: PropertiesWithDifferentAggregationEpochs
|
||||
) => {
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
refetch: refereesRefetch,
|
||||
} = useRefereesQuery({
|
||||
variables: {
|
||||
code: id as string,
|
||||
aggregationEpochs,
|
||||
},
|
||||
skip: !id,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
|
||||
const {
|
||||
data: extraData,
|
||||
loading: extraLoading,
|
||||
error: extraError,
|
||||
refetch: extraRefetch,
|
||||
} = useRefereesQuery({
|
||||
variables: {
|
||||
code: id as string,
|
||||
aggregationEpochs: propertiesOptions?.aggregationEpochs,
|
||||
},
|
||||
skip:
|
||||
// skip if the aggregation epochs are the same
|
||||
!id ||
|
||||
!propertiesOptions?.aggregationEpochs ||
|
||||
propertiesOptions.aggregationEpochs === aggregationEpochs,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
|
||||
let referees = [];
|
||||
|
||||
const refereesList = removePaginationWrapper(
|
||||
refereesData?.referralSetReferees.edges
|
||||
);
|
||||
const extraRefereesList = removePaginationWrapper(
|
||||
extraData?.referralSetReferees.edges
|
||||
);
|
||||
|
||||
referees = refereesList.map((r) =>
|
||||
overwriteProperties(r, extraRefereesList, propertiesOptions?.properties)
|
||||
);
|
||||
|
||||
const loading = refereesLoading || extraLoading;
|
||||
const error = refereesError || extraError;
|
||||
const refetch = useCallback(() => {
|
||||
refereesRefetch();
|
||||
extraRefetch();
|
||||
}, [refereesRefetch, extraRefetch]);
|
||||
|
||||
return { data: referees, loading, error, refetch };
|
||||
};
|
||||
|
||||
const overwriteProperties = (
|
||||
referee: Referee,
|
||||
referees: Referee[],
|
||||
properties?: PropertiesWithDifferentAggregationEpochs['properties']
|
||||
) => {
|
||||
let updatedProperties = {};
|
||||
const extraRefereeData = findReferee(referee.refereeId, referees);
|
||||
if (properties && extraRefereeData) {
|
||||
updatedProperties = pick(extraRefereeData, properties);
|
||||
}
|
||||
return {
|
||||
...referee,
|
||||
...updatedProperties,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,8 @@
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import omit from 'lodash/omit';
|
||||
import {
|
||||
type ReferralProgramQuery,
|
||||
useReferralProgramQuery,
|
||||
} from './__generated__/CurrentReferralProgram';
|
||||
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const MOCK = {
|
||||
@@ -86,37 +82,7 @@ const MOCK = {
|
||||
},
|
||||
};
|
||||
|
||||
type ProgramDetail = Omit<
|
||||
NonNullable<ReferralProgramQuery['currentReferralProgram']>,
|
||||
'benefitTiers' | 'stakingTiers'
|
||||
>;
|
||||
|
||||
export type BenefitTier = {
|
||||
tier: number;
|
||||
rewardFactor: number;
|
||||
commission: string;
|
||||
discountFactor: number;
|
||||
discount: string;
|
||||
minimumVolume: number;
|
||||
volume: string;
|
||||
epochs: number;
|
||||
};
|
||||
|
||||
type StakingTier = {
|
||||
tier: number;
|
||||
minimumStakedTokens: string;
|
||||
referralRewardMultiplier: string;
|
||||
};
|
||||
|
||||
export type ReferralProgramData = {
|
||||
benefitTiers: BenefitTier[];
|
||||
stakingTiers: StakingTier[];
|
||||
details: ProgramDetail | undefined;
|
||||
loading: boolean;
|
||||
error?: ApolloError;
|
||||
};
|
||||
|
||||
export const useReferralProgram = (): ReferralProgramData => {
|
||||
export const useReferralProgram = () => {
|
||||
const { data, loading, error } = useReferralProgramQuery({
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
@@ -5,22 +5,20 @@ import {
|
||||
ToastHeading,
|
||||
Button,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useReferral } from './use-referral';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useT } from '../../../lib/use-t';
|
||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../../lib/links';
|
||||
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
||||
import { useFindReferralSet } from './use-find-referral-set';
|
||||
|
||||
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
|
||||
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
|
||||
|
||||
const useNonEligibleReferralSet = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data, loading, role, isEligible, refetch } =
|
||||
useFindReferralSet(pubKey);
|
||||
|
||||
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
|
||||
const {
|
||||
data: epochData,
|
||||
loading: epochLoading,
|
||||
@@ -38,13 +36,7 @@ const useNonEligibleReferralSet = () => {
|
||||
};
|
||||
}, [epochRefetch, refetch]);
|
||||
|
||||
return {
|
||||
data,
|
||||
isEligible,
|
||||
role,
|
||||
epoch: epochData?.epoch.id,
|
||||
loading: loading || epochLoading,
|
||||
};
|
||||
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
|
||||
};
|
||||
|
||||
export const useReferralToasts = () => {
|
||||
@@ -57,16 +49,14 @@ export const useReferralToasts = () => {
|
||||
store.update,
|
||||
]);
|
||||
|
||||
const { data, role, isEligible, epoch, loading } =
|
||||
useNonEligibleReferralSet();
|
||||
const { data, epoch, loading } = useNonEligibleReferralSet();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
data &&
|
||||
role === 'referee' &&
|
||||
epoch &&
|
||||
!loading &&
|
||||
!isEligible &&
|
||||
!data.isEligible &&
|
||||
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
|
||||
) {
|
||||
const nonEligibleReferralToast: Toast = {
|
||||
@@ -108,11 +98,9 @@ export const useReferralToasts = () => {
|
||||
data,
|
||||
epoch,
|
||||
hasToast,
|
||||
isEligible,
|
||||
loading,
|
||||
navigate,
|
||||
pathname,
|
||||
role,
|
||||
setToast,
|
||||
t,
|
||||
updateToast,
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { useRefereesQuery } from './__generated__/Referees';
|
||||
import compact from 'lodash/compact';
|
||||
import pick from 'lodash/pick';
|
||||
import type {
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables,
|
||||
} from './__generated__/ReferralSets';
|
||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
|
||||
|
||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||
|
||||
export type Role = 'referrer' | 'referee';
|
||||
type UseReferralArgs = (
|
||||
| { code: string | undefined }
|
||||
| { pubKey: string | undefined; role: Role }
|
||||
) & {
|
||||
aggregationEpochs?: number;
|
||||
};
|
||||
|
||||
const prepareVariables = (
|
||||
args: UseReferralArgs
|
||||
): [ReferralSetsQueryVariables, boolean] => {
|
||||
const byCode = 'code' in args;
|
||||
const byRole = 'pubKey' in args && 'role' in args;
|
||||
let variables = {};
|
||||
let skip = true;
|
||||
if (byCode) {
|
||||
variables = {
|
||||
id: args.code,
|
||||
};
|
||||
skip = !args.code;
|
||||
}
|
||||
if (byRole) {
|
||||
if (args.role === 'referee') {
|
||||
variables = { referee: args.pubKey };
|
||||
}
|
||||
if (args.role === 'referrer') {
|
||||
variables = { referrer: args.pubKey };
|
||||
}
|
||||
skip = !args.pubKey;
|
||||
}
|
||||
|
||||
return [variables, skip];
|
||||
};
|
||||
|
||||
export const useReferral = (args: UseReferralArgs) => {
|
||||
const [variables, skip] = prepareVariables(args);
|
||||
|
||||
const {
|
||||
data: referralData,
|
||||
loading: referralLoading,
|
||||
error: referralError,
|
||||
refetch: referralRefetch,
|
||||
} = useReferralSetsQuery({
|
||||
variables,
|
||||
skip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
// A user can only have 1 active referral program at a time
|
||||
const referralSet =
|
||||
referralData?.referralSets.edges &&
|
||||
referralData.referralSets.edges.length > 0
|
||||
? referralData.referralSets.edges[0]?.node
|
||||
: undefined;
|
||||
|
||||
const { isEligible } = useStakeAvailable(referralSet?.referrer);
|
||||
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
refetch: refereesRefetch,
|
||||
} = useRefereesQuery({
|
||||
variables: {
|
||||
code: referralSet?.id as string,
|
||||
aggregationEpochs:
|
||||
args.aggregationEpochs !== null
|
||||
? args.aggregationEpochs
|
||||
: DEFAULT_AGGREGATION_DAYS,
|
||||
},
|
||||
skip: !referralSet?.id,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
|
||||
const referees = compact(
|
||||
removePaginationWrapper(refereesData?.referralSetReferees.edges)
|
||||
);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
referralRefetch();
|
||||
refereesRefetch();
|
||||
}, [refereesRefetch, referralRefetch]);
|
||||
|
||||
const byReferee =
|
||||
'role' in args && 'pubKey' in args && args.role === 'referee';
|
||||
const referee = byReferee
|
||||
? referees.find((r) => r.refereeId === args.pubKey) || null
|
||||
: null;
|
||||
|
||||
const data =
|
||||
referralSet && refereesData
|
||||
? {
|
||||
code: referralSet.id,
|
||||
role: 'role' in args ? args.role : null,
|
||||
referee: referee,
|
||||
referrerId: referralSet.referrer,
|
||||
createdAt: referralSet.createdAt,
|
||||
isEligible,
|
||||
referees,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
data,
|
||||
loading: referralLoading || refereesLoading,
|
||||
error: referralError || refereesError,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
type Referee = NonNullable<
|
||||
NonNullable<ReturnType<typeof useReferral>['data']>['referee']
|
||||
>;
|
||||
|
||||
type RefereeProperties = (keyof Referee)[];
|
||||
|
||||
const findReferee = (referee: Referee, referees: Referee[]) =>
|
||||
referees.find((r) => r.refereeId === referee?.refereeId) || referee;
|
||||
|
||||
const updateReferee = (
|
||||
referee: Referee,
|
||||
referees: Referee[],
|
||||
properties: RefereeProperties
|
||||
) => ({
|
||||
...referee,
|
||||
...pick(findReferee(referee, referees), properties),
|
||||
});
|
||||
|
||||
export const useUpdateReferees = (
|
||||
referral: ReturnType<typeof useReferral>,
|
||||
aggregationEpochs: number,
|
||||
properties: RefereeProperties,
|
||||
skip?: boolean
|
||||
): ReturnType<typeof useReferral> => {
|
||||
const { data, loading, error, refetch } = useRefereesQuery({
|
||||
variables: {
|
||||
code: referral?.data?.code as string,
|
||||
aggregationEpochs,
|
||||
},
|
||||
skip: skip || !referral?.data?.code,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
const refetchAll = useCallback(() => {
|
||||
refetch();
|
||||
referral.refetch();
|
||||
}, [refetch, referral]);
|
||||
if (!referral.data || skip) {
|
||||
return referral;
|
||||
}
|
||||
const referees = compact(
|
||||
removePaginationWrapper(data?.referralSetReferees.edges)
|
||||
);
|
||||
|
||||
return {
|
||||
data: data && {
|
||||
...referral.data,
|
||||
referees: referral.data.referees.map((referee) =>
|
||||
updateReferee(referee, referees, properties)
|
||||
),
|
||||
referee:
|
||||
referral.data.referee &&
|
||||
updateReferee(referral.data.referee, referees, properties),
|
||||
},
|
||||
loading: loading || referral.loading,
|
||||
error: error || referral.error,
|
||||
refetch: refetchAll,
|
||||
};
|
||||
};
|
||||
|
||||
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
|
||||
data?.referralSets.edges && data.referralSets.edges.length > 0
|
||||
? data.referralSets.edges[0]?.node
|
||||
: undefined;
|
||||
|
||||
export const useIsInReferralSet = (pubKey: string | undefined) => {
|
||||
const [asRefereeVariables, asRefereeSkip] = prepareVariables({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
});
|
||||
const [asReferrerVariables, asReferrerSkip] = prepareVariables({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
|
||||
const { data: asRefereeData } = useReferralSetsQuery({
|
||||
variables: asRefereeVariables,
|
||||
skip: asRefereeSkip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const { data: asReferrerData } = useReferralSetsQuery({
|
||||
variables: asReferrerVariables,
|
||||
skip: asReferrerSkip,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
return Boolean(
|
||||
retrieveReferralSetData(asRefereeData) ||
|
||||
retrieveReferralSetData(asReferrerData)
|
||||
);
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useReferees } from './use-referees';
|
||||
import { type StatValue } from '../constants';
|
||||
|
||||
export type ReferrerStats = {
|
||||
/** the base commission -> `rewardFactor` ~ `referralRewardFactor` */
|
||||
baseCommission: StatValue<BigNumber>;
|
||||
/** the staking multiplier -> `rewardsMultiplier` ~ `referralRewardMultiplier` */
|
||||
multiplier: StatValue<BigNumber>;
|
||||
/** the final commission -> base * multiplier */
|
||||
finalCommission: StatValue<BigNumber>;
|
||||
/** the referrer taker volume -> `referrerTakerVolume` */
|
||||
volume: StatValue<BigNumber>;
|
||||
/** the number of referees -> referees query required */
|
||||
referees: StatValue<BigNumber>;
|
||||
/** the total commission -> sum of `totalRefereeGeneratedRewards` */
|
||||
totalCommission: StatValue<BigNumber>;
|
||||
runningVolume: StatValue<BigNumber>;
|
||||
};
|
||||
|
||||
const ZERO = BigNumber(0);
|
||||
const ONE = BigNumber(1);
|
||||
|
||||
export const useReferrerStats = (
|
||||
setId: string,
|
||||
aggregationEpochs: number
|
||||
): ReferrerStats => {
|
||||
const { data, loading, error } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: setId,
|
||||
},
|
||||
skip: !setId || setId.length === 0,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const {
|
||||
data: refereesData,
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
} = useReferees(setId, aggregationEpochs);
|
||||
|
||||
const statsAvailable = data?.referralSetStats.edges[0]?.node;
|
||||
|
||||
const baseCommission = {
|
||||
value: statsAvailable ? BigNumber(statsAvailable.rewardFactor) : ZERO,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
const multiplier = {
|
||||
value: statsAvailable ? BigNumber(statsAvailable.rewardsMultiplier) : ONE,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
const finalCommission = {
|
||||
value: !multiplier.value.isNaN()
|
||||
? baseCommission.value
|
||||
: new BigNumber(multiplier.value).times(baseCommission.value),
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
const volume = {
|
||||
value: statsAvailable
|
||||
? BigNumber(statsAvailable.referrerTakerVolume)
|
||||
: ZERO,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
const referees = {
|
||||
value: BigNumber(refereesData.length),
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
};
|
||||
|
||||
const totalCommission = {
|
||||
value: refereesData
|
||||
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
||||
.reduce((all, r) => all.plus(r), ZERO),
|
||||
loading: refereesLoading,
|
||||
error: refereesError,
|
||||
};
|
||||
|
||||
const runningVolume = {
|
||||
value: statsAvailable?.referralSetRunningNotionalTakerVolume
|
||||
? BigNumber(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||
: ZERO,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
return {
|
||||
baseCommission,
|
||||
multiplier,
|
||||
finalCommission,
|
||||
volume,
|
||||
referees,
|
||||
totalCommission,
|
||||
runningVolume,
|
||||
};
|
||||
};
|
||||
@@ -1,202 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import { useRefereeStats } from './hooks/use-referee-stats';
|
||||
import {
|
||||
BenefitTierTile,
|
||||
DiscountTile,
|
||||
EpochsTile,
|
||||
NextTierEpochsTile,
|
||||
NextTierVolumeTile,
|
||||
RunningVolumeTile,
|
||||
TeamTile,
|
||||
} from './tiles';
|
||||
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||
import { CodeTile } from './tile';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ApplyCodeForm } from './apply-code-form';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
||||
import { useReferralSet } from './hooks/use-find-referral-set';
|
||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import minBy from 'lodash/minBy';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const RefereeStatistics = ({
|
||||
aggregationEpochs,
|
||||
setId,
|
||||
pubKey,
|
||||
referrerPubKey,
|
||||
}: {
|
||||
/** The aggregation epochs used to calculate statistics. */
|
||||
aggregationEpochs: number;
|
||||
/** The set id (code). */
|
||||
setId: string;
|
||||
/** The referee public key. */
|
||||
pubKey: string;
|
||||
/** The referrer's public key. */
|
||||
referrerPubKey: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const {
|
||||
benefitTier,
|
||||
discountFactor,
|
||||
epochs,
|
||||
nextBenefitTier,
|
||||
runningVolume,
|
||||
} = useRefereeStats(pubKey, setId, aggregationEpochs);
|
||||
|
||||
const { isEligible } = useStakeAvailable(referrerPubKey);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-testid="referral-statistics"
|
||||
data-as="referee"
|
||||
className="relative mx-auto mb-20"
|
||||
>
|
||||
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
||||
{/** TEAM TILE - referral set id is the same as team id */}
|
||||
<TeamTile teamId={setId} />
|
||||
{/** TILES ROW 1 */}
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
<BenefitTierTile
|
||||
benefitTier={benefitTier}
|
||||
nextBenefitTier={nextBenefitTier}
|
||||
/>
|
||||
<RunningVolumeTile
|
||||
aggregationEpochs={aggregationEpochs}
|
||||
runningVolume={runningVolume}
|
||||
/>
|
||||
<CodeTile code={setId} />
|
||||
</div>
|
||||
{/** TILES ROW 2 */}
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<DiscountTile discountFactor={discountFactor} />
|
||||
<NextTierVolumeTile
|
||||
nextBenefitTier={nextBenefitTier}
|
||||
runningVolume={runningVolume}
|
||||
/>
|
||||
<EpochsTile epochs={epochs} />
|
||||
<NextTierEpochsTile
|
||||
epochs={epochs}
|
||||
nextBenefitTier={nextBenefitTier}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/** ELIGIBILITY WARNING */}
|
||||
{!isEligible ? (
|
||||
<div
|
||||
data-testid="referral-eligibility-warning"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
||||
>
|
||||
<h2 className="text-2xl mb-2">
|
||||
{t('Referral code no longer valid')}
|
||||
</h2>
|
||||
<p>
|
||||
{t(
|
||||
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{!isEligible && <ApplyCodeForm />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PreviewRefereeStatistics = ({ setId }: { setId: string }) => {
|
||||
const program = useReferralProgram();
|
||||
const aggregationEpochs =
|
||||
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
|
||||
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data: referralSet, loading } = useReferralSet(setId);
|
||||
|
||||
const { epochs, runningVolume } = useRefereeStats(
|
||||
pubKey || '',
|
||||
referralSet?.id || '',
|
||||
aggregationEpochs
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
data-testid="referral-statistics"
|
||||
data-as="referee"
|
||||
data-preview
|
||||
className="relative mx-auto mb-20"
|
||||
>
|
||||
<Loader size="small" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!referralSet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stat = <T,>(value: T) => ({
|
||||
value,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const firstBenefitTier = stat(minBy(program.benefitTiers, (bt) => bt.epochs));
|
||||
|
||||
const nextBenefitTier = stat(
|
||||
program.benefitTiers.find(
|
||||
(bt) =>
|
||||
bt.tier ===
|
||||
(firstBenefitTier.value?.tier
|
||||
? firstBenefitTier.value.tier + 1
|
||||
: undefined)
|
||||
)
|
||||
);
|
||||
|
||||
const discountFactor = stat(
|
||||
firstBenefitTier.value?.discountFactor
|
||||
? BigNumber(firstBenefitTier.value?.discountFactor)
|
||||
: BigNumber(0)
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="referral-statistics"
|
||||
data-as="referee"
|
||||
data-preview
|
||||
className="relative mx-auto mb-20"
|
||||
>
|
||||
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
||||
{/** TEAM TILE - referral set id is the same as team id */}
|
||||
<TeamTile teamId={setId} />
|
||||
{/** TILES ROW 1 */}
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
<BenefitTierTile
|
||||
benefitTier={firstBenefitTier}
|
||||
nextBenefitTier={nextBenefitTier}
|
||||
/>
|
||||
<RunningVolumeTile
|
||||
aggregationEpochs={aggregationEpochs}
|
||||
runningVolume={runningVolume}
|
||||
/>
|
||||
<CodeTile code={setId} />
|
||||
</div>
|
||||
{/** TILES ROW 2 */}
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<DiscountTile discountFactor={discountFactor} />
|
||||
<NextTierVolumeTile
|
||||
nextBenefitTier={nextBenefitTier}
|
||||
runningVolume={runningVolume}
|
||||
/>
|
||||
<EpochsTile epochs={epochs} />
|
||||
<NextTierEpochsTile
|
||||
epochs={epochs}
|
||||
nextBenefitTier={nextBenefitTier}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { ns, useT } from '../../lib/use-t';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Loader,
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Table } from '../../components/table';
|
||||
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { QUSDTooltip } from './qusd-tooltip';
|
||||
import { type Referee, useReferees } from './hooks/use-referees';
|
||||
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
||||
|
||||
export const Referees = ({
|
||||
setId,
|
||||
aggregationEpochs,
|
||||
}: {
|
||||
setId: string;
|
||||
aggregationEpochs: number;
|
||||
}) => {
|
||||
const { data, loading } = useReferees(setId, aggregationEpochs, {
|
||||
// get total referree generated rewards for the last 30 days
|
||||
aggregationEpochs: DEFAULT_AGGREGATION_DAYS,
|
||||
properties: ['totalRefereeGeneratedRewards'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Loader size="small" />;
|
||||
}
|
||||
|
||||
return <RefereesTable data={data} aggregationEpochs={aggregationEpochs} />;
|
||||
};
|
||||
|
||||
export const RefereesTable = ({
|
||||
data: referees,
|
||||
aggregationEpochs,
|
||||
}: {
|
||||
data: Referee[];
|
||||
aggregationEpochs: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||
setCollapsed(true);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{/* Referees (only for referrer view) */}
|
||||
{referees.length > 0 && (
|
||||
<div className="mt-20 mb-20">
|
||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||
<div
|
||||
className={classNames(
|
||||
collapsed && [
|
||||
'relative max-h-96 overflow-hidden',
|
||||
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
|
||||
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
|
||||
]
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
|
||||
{
|
||||
hidden: !collapsed,
|
||||
}
|
||||
)}
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
|
||||
</button>
|
||||
<Table
|
||||
ref={tableRef}
|
||||
columns={[
|
||||
{ name: 'party', displayName: t('Trader') },
|
||||
{ name: 'joined', displayName: t('Date Joined') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'volumeLastEpochs',
|
||||
'Volume (last {{count}} epochs)',
|
||||
{
|
||||
count: aggregationEpochs,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
// NOTE: This should be gotten for the last 30 days regardless of the program's window length
|
||||
name: 'commission',
|
||||
displayName: (
|
||||
<Trans
|
||||
i18nKey="referralStatisticsCommission"
|
||||
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
||||
components={[
|
||||
<QUSDTooltip key="0" />,
|
||||
<Tooltip
|
||||
key="1"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
<span>last 30 epochs</span>
|
||||
</Tooltip>,
|
||||
]}
|
||||
values={{
|
||||
count: DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
ns={ns}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={sortBy(
|
||||
referees.map((r) => ({
|
||||
party: (
|
||||
<span title={r.refereeId}>
|
||||
{truncateMiddle(r.refereeId)}
|
||||
</span>
|
||||
),
|
||||
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
|
||||
volume: Number(r.totalRefereeNotionalTakerVolume),
|
||||
commission: Number(r.totalRefereeGeneratedRewards),
|
||||
})),
|
||||
(r) => r.volume
|
||||
)
|
||||
.map((r) => ({
|
||||
...r,
|
||||
volume: formatNumber(r.volume, 0),
|
||||
commission: formatNumber(r.commission, 0),
|
||||
}))
|
||||
.reverse()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +1,597 @@
|
||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import minBy from 'lodash/minBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import compact from 'lodash/compact';
|
||||
import { Trans } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
TextChildrenTooltip as Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||
import { ApplyCodeFormContainer } from './apply-code-form';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
getDateFormat,
|
||||
getDateTimeFormat,
|
||||
getUserLocale,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
||||
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { useTeam } from '../../lib/hooks/use-team';
|
||||
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||
import { TeamStats } from '../../components/competitions/team-stats';
|
||||
import { Table } from '../../components/table';
|
||||
import {
|
||||
DEFAULT_AGGREGATION_DAYS,
|
||||
useReferral,
|
||||
useUpdateReferees,
|
||||
} from './hooks/use-referral';
|
||||
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { useFindReferralSet } from './hooks/use-find-referral-set';
|
||||
import { Referees } from './referees';
|
||||
import { ReferrerStatistics } from './referrer-statistics';
|
||||
import { RefereeStatistics } from './referee-statistics';
|
||||
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
||||
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||
import { QUSDTooltip } from './qusd-tooltip';
|
||||
import { CodeTile, StatTile, Tile } from './tile';
|
||||
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const program = useReferralProgram();
|
||||
|
||||
const {
|
||||
data: referralSet,
|
||||
loading: referralSetLoading,
|
||||
role,
|
||||
refetch,
|
||||
} = useFindReferralSet(pubKey);
|
||||
const { data: referee, refetch: refereeRefetch } = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
if (referralSetLoading) {
|
||||
return <Loader size="small" />;
|
||||
}
|
||||
const { data: referrer, refetch: referrerRefetch } = useUpdateReferees(
|
||||
useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
}),
|
||||
DEFAULT_AGGREGATION_DAYS,
|
||||
['totalRefereeGeneratedRewards'],
|
||||
DEFAULT_AGGREGATION_DAYS === program.details?.windowLength
|
||||
);
|
||||
|
||||
const aggregationEpochs =
|
||||
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
|
||||
const refetch = useCallback(() => {
|
||||
refereeRefetch();
|
||||
referrerRefetch();
|
||||
}, [refereeRefetch, referrerRefetch]);
|
||||
|
||||
if (referralSet?.id && role === 'referrer') {
|
||||
if (referee?.code) {
|
||||
return (
|
||||
<>
|
||||
<ReferrerStatistics
|
||||
aggregationEpochs={aggregationEpochs}
|
||||
createdAt={referralSet.createdAt}
|
||||
setId={referralSet.id}
|
||||
/>
|
||||
<Referees
|
||||
setId={referralSet.id}
|
||||
aggregationEpochs={aggregationEpochs}
|
||||
/>
|
||||
<Statistics data={referee} program={program} as="referee" />
|
||||
{!referee.isEligible && <ApplyCodeForm />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (pubKey && referralSet?.id && role === 'referee') {
|
||||
if (referrer?.code) {
|
||||
return (
|
||||
<RefereeStatistics
|
||||
aggregationEpochs={aggregationEpochs}
|
||||
pubKey={pubKey}
|
||||
referrerPubKey={referralSet.referrer}
|
||||
setId={referralSet.id}
|
||||
/>
|
||||
<>
|
||||
<Statistics data={referrer} program={program} as="referrer" />
|
||||
<RefereesTable data={referrer} program={program} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <ApplyCodeFormContainer onSuccess={refetch} />;
|
||||
};
|
||||
|
||||
export const useStats = ({
|
||||
data,
|
||||
program,
|
||||
}: {
|
||||
data?: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
}) => {
|
||||
const { benefitTiers } = program;
|
||||
const { data: epochData } = useEpochInfoQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
variables: {
|
||||
code: data?.code || '',
|
||||
},
|
||||
skip: !data?.code,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
const stats =
|
||||
statsData?.referralSetStats.edges &&
|
||||
compact(removePaginationWrapper(statsData.referralSetStats.edges));
|
||||
const refereeInfo = data?.referee;
|
||||
const refereeStats = stats?.find(
|
||||
(r) => r.partyId === data?.referee?.refereeId
|
||||
);
|
||||
|
||||
const statsAvailable = stats && stats.length > 0 && stats[0];
|
||||
const baseCommissionValue = statsAvailable
|
||||
? Number(statsAvailable.rewardFactor)
|
||||
: 0;
|
||||
const runningVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||
: 0;
|
||||
const referrerVolumeValue = statsAvailable
|
||||
? Number(statsAvailable.referrerTakerVolume)
|
||||
: 0;
|
||||
const multiplier = statsAvailable
|
||||
? Number(statsAvailable.rewardsMultiplier)
|
||||
: 1;
|
||||
const finalCommissionValue = isNaN(multiplier)
|
||||
? baseCommissionValue
|
||||
: new BigNumber(multiplier).times(baseCommissionValue).toNumber();
|
||||
|
||||
const discountFactorValue = refereeStats?.discountFactor
|
||||
? Number(refereeStats.discountFactor)
|
||||
: 0;
|
||||
const currentBenefitTierValue = benefitTiers.find(
|
||||
(t) =>
|
||||
!isNaN(discountFactorValue) &&
|
||||
!isNaN(t.discountFactor) &&
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
|
||||
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
: 0;
|
||||
const nextBenefitTierVolumeValue = nextBenefitTierValue
|
||||
? nextBenefitTierValue.minimumVolume - runningVolumeValue
|
||||
: 0;
|
||||
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
||||
? nextBenefitTierValue.epochs - epochsValue
|
||||
: 0;
|
||||
|
||||
return {
|
||||
baseCommissionValue,
|
||||
runningVolumeValue,
|
||||
referrerVolumeValue,
|
||||
multiplier,
|
||||
finalCommissionValue,
|
||||
discountFactorValue,
|
||||
currentBenefitTierValue,
|
||||
nextBenefitTierValue,
|
||||
epochsValue,
|
||||
nextBenefitTierVolumeValue,
|
||||
nextBenefitTierEpochsValue,
|
||||
};
|
||||
};
|
||||
|
||||
export const Statistics = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
as: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const t = useT();
|
||||
const {
|
||||
baseCommissionValue,
|
||||
runningVolumeValue,
|
||||
referrerVolumeValue,
|
||||
multiplier,
|
||||
finalCommissionValue,
|
||||
discountFactorValue,
|
||||
currentBenefitTierValue,
|
||||
epochsValue,
|
||||
nextBenefitTierValue,
|
||||
nextBenefitTierVolumeValue,
|
||||
nextBenefitTierEpochsValue,
|
||||
} = useStats({ data, program });
|
||||
|
||||
const isApplyCodePreview = data.referee === null;
|
||||
|
||||
const { benefitTiers } = useReferralProgram();
|
||||
|
||||
const { stakeAvailable, isEligible } = useStakeAvailable();
|
||||
const { details } = program;
|
||||
|
||||
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
const baseCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Base commission rate')}
|
||||
description={t(
|
||||
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
|
||||
{
|
||||
runningVolume: compactNumFormat.format(runningVolumeValue),
|
||||
epochs: (
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||
).toString(),
|
||||
}
|
||||
)}
|
||||
testId="base-commission-rate"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{baseCommissionValue * 100}%
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const stakingMultiplierTile = (
|
||||
<StatTile
|
||||
title={t('Staking multiplier')}
|
||||
testId="staking-multiplier"
|
||||
description={
|
||||
<span
|
||||
className={classNames({
|
||||
'text-vega-red': !isEligible,
|
||||
})}
|
||||
>
|
||||
{t('{{amount}} $VEGA staked', {
|
||||
amount: addDecimalsFormatNumber(
|
||||
stakeAvailable?.toString() || 0,
|
||||
18
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{multiplier || t('None')}
|
||||
</StatTile>
|
||||
);
|
||||
const baseCommissionFormatted = BigNumber(baseCommissionValue)
|
||||
.times(100)
|
||||
.toString();
|
||||
const finalCommissionFormatted = new BigNumber(finalCommissionValue)
|
||||
.times(100)
|
||||
.toString();
|
||||
const finalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={
|
||||
!isNaN(multiplier)
|
||||
? `(${baseCommissionFormatted}% ⨉ ${multiplier} = ${finalCommissionFormatted}%)`
|
||||
: undefined
|
||||
}
|
||||
testId="final-commission-rate"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{finalCommissionFormatted}%
|
||||
</StatTile>
|
||||
);
|
||||
const numberOfTradersValue = data.referees.length;
|
||||
const numberOfTradersTile = (
|
||||
<StatTile title={t('Number of traders')} testId="number-of-traders">
|
||||
{numberOfTradersValue}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const codeTile = (
|
||||
<CodeTile
|
||||
code={data?.code}
|
||||
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
||||
/>
|
||||
);
|
||||
|
||||
const referrerVolumeTile = (
|
||||
<StatTile
|
||||
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
})}
|
||||
testId="my-volume"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{compactNumFormat.format(referrerVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const totalCommissionValue = data.referees
|
||||
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
testId="total-commission"
|
||||
title={
|
||||
<Trans
|
||||
i18nKey="totalCommission"
|
||||
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
||||
values={{
|
||||
count: DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
components={[
|
||||
<Tooltip
|
||||
key="1"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
last 30 epochs
|
||||
</Tooltip>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
description={<QUSDTooltip />}
|
||||
>
|
||||
{formatNumber(totalCommissionValue, 0)}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile
|
||||
title={t('Current tier')}
|
||||
testId="current-tier"
|
||||
description={
|
||||
nextBenefitTierValue?.tier
|
||||
? t('(Next tier: {{nextTier}})', {
|
||||
nextTier: nextBenefitTierValue?.tier,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{isApplyCodePreview
|
||||
? currentBenefitTierValue?.tier || benefitTiers[0]?.tier || 'None'
|
||||
: currentBenefitTierValue?.tier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
<StatTile
|
||||
title={t('Discount')}
|
||||
testId="discount"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{isApplyCodePreview && benefitTiers.length >= 1
|
||||
? benefitTiers[0].discountFactor * 100
|
||||
: discountFactorValue * 100}
|
||||
%
|
||||
</StatTile>
|
||||
);
|
||||
const runningVolumeTile = (
|
||||
<StatTile
|
||||
title={t(
|
||||
'runningNotionalOverEpochs',
|
||||
'Combined volume (last {{count}} epochs)',
|
||||
{
|
||||
count: details?.windowLength,
|
||||
}
|
||||
)}
|
||||
testId="combined-volume"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const epochsTile = (
|
||||
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
|
||||
{epochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile
|
||||
title={t('Volume to next tier')}
|
||||
testId="vol-to-next-tier"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile
|
||||
title={t('Epochs to next tier')}
|
||||
testId="epochs-to-next-tier"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
|
||||
<div
|
||||
data-testid="referral-eligibility-warning"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
||||
>
|
||||
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
||||
<p>
|
||||
{t(
|
||||
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const referrerTiles = (
|
||||
<>
|
||||
<Team teamId={data.code} />
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
{baseCommissionTile}
|
||||
{stakingMultiplierTile}
|
||||
{finalCommissionTile}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{codeTile}
|
||||
{referrerVolumeTile}
|
||||
{numberOfTradersTile}
|
||||
{totalCommissionTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const refereeTiles = (
|
||||
<>
|
||||
<Team teamId={data.code} />
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
{currentBenefitTierTile}
|
||||
{runningVolumeTile}
|
||||
{codeTile}
|
||||
</div>
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{discountFactorTile}
|
||||
{nextTierVolumeTile}
|
||||
{epochsTile}
|
||||
{nextTierEpochsTile}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="referral-statistics"
|
||||
data-as={as}
|
||||
className="relative mx-auto mb-20"
|
||||
>
|
||||
<div
|
||||
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
|
||||
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
|
||||
})}
|
||||
>
|
||||
{as === 'referrer' && referrerTiles}
|
||||
{as === 'referee' && refereeTiles}
|
||||
</div>
|
||||
{eligibilityWarningOverlay}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const RefereesTable = ({
|
||||
data,
|
||||
program,
|
||||
}: {
|
||||
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const { details } = program;
|
||||
useLayoutEffect(() => {
|
||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||
setCollapsed(true);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{/* Referees (only for referrer view) */}
|
||||
{data.referees.length > 0 && (
|
||||
<div className="mt-20 mb-20">
|
||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||
<div
|
||||
className={classNames(
|
||||
collapsed && [
|
||||
'relative max-h-96 overflow-hidden',
|
||||
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
|
||||
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
|
||||
]
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
|
||||
{
|
||||
hidden: !collapsed,
|
||||
}
|
||||
)}
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
|
||||
</button>
|
||||
<Table
|
||||
ref={tableRef}
|
||||
columns={[
|
||||
{ name: 'party', displayName: t('Trader') },
|
||||
{ name: 'joined', displayName: t('Date Joined') },
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'volumeLastEpochs',
|
||||
'Volume (last {{count}} epochs)',
|
||||
{
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'commission',
|
||||
displayName: (
|
||||
<Trans
|
||||
i18nKey="referralStatisticsCommission"
|
||||
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
||||
components={[
|
||||
<QUSDTooltip key="0" />,
|
||||
<Tooltip
|
||||
key="1"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
last 30 epochs
|
||||
</Tooltip>,
|
||||
]}
|
||||
values={{
|
||||
count: DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
ns={ns}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={sortBy(
|
||||
data.referees.map((r) => ({
|
||||
party: (
|
||||
<span title={r.refereeId}>
|
||||
{truncateMiddle(r.refereeId)}
|
||||
</span>
|
||||
),
|
||||
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
|
||||
volume: Number(r.totalRefereeNotionalTakerVolume),
|
||||
commission: Number(r.totalRefereeGeneratedRewards),
|
||||
})),
|
||||
(r) => r.volume
|
||||
)
|
||||
.map((r) => ({
|
||||
...r,
|
||||
volume: formatNumber(r.volume, 0),
|
||||
commission: formatNumber(r.commission, 0),
|
||||
}))
|
||||
.reverse()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Team = ({ teamId }: { teamId?: string }) => {
|
||||
const { team, members } = useTeam(teamId);
|
||||
const { data: games } = useGames(teamId);
|
||||
|
||||
if (!team) return null;
|
||||
|
||||
return (
|
||||
<Tile className="flex gap-3 lg:gap-4">
|
||||
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
||||
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
|
||||
<TeamStats
|
||||
members={members}
|
||||
games={areTeamGames(games) ? games : undefined}
|
||||
/>
|
||||
</div>
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,12 +10,12 @@ import { TabLink } from './buttons';
|
||||
import { Outlet, useMatch } from 'react-router-dom';
|
||||
import { Routes } from '../../lib/links';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||
import { useReferral } from './hooks/use-referral';
|
||||
import { REFERRAL_DOCS_LINK } from './constants';
|
||||
import classNames from 'classnames';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||
import { useFindReferralSet } from './hooks/use-find-referral-set';
|
||||
|
||||
const Nav = () => {
|
||||
const t = useT();
|
||||
@@ -34,9 +34,26 @@ export const Referrals = () => {
|
||||
const t = useT();
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data, loading, error } = useFindReferralSet(pubKey);
|
||||
const {
|
||||
data: referee,
|
||||
loading: refereeLoading,
|
||||
error: refereeError,
|
||||
} = useReferral({
|
||||
pubKey,
|
||||
role: 'referee',
|
||||
});
|
||||
const {
|
||||
data: referrer,
|
||||
loading: referrerLoading,
|
||||
error: referrerError,
|
||||
} = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
});
|
||||
|
||||
const showNav = !loading && !error && !data;
|
||||
const error = refereeError || referrerError;
|
||||
const loading = refereeLoading || referrerLoading;
|
||||
const showNav = !loading && !error && !referrer && !referee;
|
||||
|
||||
usePageTitle(t('Referrals'));
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import { useReferrerStats } from './hooks/use-referrer-stats';
|
||||
import {
|
||||
BaseCommissionTile,
|
||||
FinalCommissionTile,
|
||||
RefereesTile,
|
||||
StakingMultiplierTile,
|
||||
TeamTile,
|
||||
TotalCommissionTile,
|
||||
VolumeTile,
|
||||
dateFormatter,
|
||||
} from './tiles';
|
||||
import { CodeTile } from './tile';
|
||||
|
||||
export const ReferrerStatistics = ({
|
||||
aggregationEpochs,
|
||||
setId,
|
||||
createdAt,
|
||||
}: {
|
||||
/** The aggregation epochs used to calculate statistics. */
|
||||
aggregationEpochs: number;
|
||||
/** The set id (code). */
|
||||
setId: string;
|
||||
/** The referral set date of creation. */
|
||||
createdAt: string;
|
||||
}) => {
|
||||
const {
|
||||
baseCommission,
|
||||
finalCommission,
|
||||
multiplier,
|
||||
referees,
|
||||
runningVolume,
|
||||
totalCommission,
|
||||
volume,
|
||||
} = useReferrerStats(setId, aggregationEpochs);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="referral-statistics"
|
||||
data-as="referrer"
|
||||
className="relative mx-auto mb-20"
|
||||
>
|
||||
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
||||
{/** TEAM TILE - referral set id is the same as team id */}
|
||||
<TeamTile teamId={setId} />
|
||||
{/** TILES ROW 1 */}
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||
<BaseCommissionTile
|
||||
aggregationEpochs={aggregationEpochs}
|
||||
baseCommission={baseCommission}
|
||||
runningVolume={runningVolume}
|
||||
/>
|
||||
<StakingMultiplierTile multiplier={multiplier} />
|
||||
<FinalCommissionTile
|
||||
baseCommission={baseCommission}
|
||||
multiplier={multiplier}
|
||||
finalCommission={finalCommission}
|
||||
/>
|
||||
</div>
|
||||
{/** TILES ROW 2 */}
|
||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<CodeTile code={setId} createdAt={dateFormatter(createdAt)} />
|
||||
<VolumeTile aggregationEpochs={aggregationEpochs} volume={volume} />
|
||||
<RefereesTile referees={referees} />
|
||||
<TotalCommissionTile
|
||||
aggregationEpochs={aggregationEpochs}
|
||||
totalCommission={totalCommission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,440 +0,0 @@
|
||||
import { addDecimalsFormatNumber, getDateFormat } from '@vegaprotocol/utils';
|
||||
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import classNames from 'classnames';
|
||||
import { type ReactNode } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { type StatValue, COMPACT_NUMBER_FORMAT } from './constants';
|
||||
import { type ReferrerStats } from './hooks/use-referrer-stats';
|
||||
import { type RefereeStats } from './hooks/use-referee-stats';
|
||||
import { QUSDTooltip } from './qusd-tooltip';
|
||||
import { NoProgramTile, StatTile, Tile } from './tile';
|
||||
import { Loader, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { type BenefitTier } from './hooks/use-referral-program';
|
||||
import { useTeam } from '../../lib/hooks/use-team';
|
||||
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
|
||||
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||
import { TeamStats } from '../../components/competitions/team-stats';
|
||||
|
||||
/* Formatters */
|
||||
|
||||
const percentageFormatter = (value: BigNumber) =>
|
||||
value.times(100).toFixed(2) + '%';
|
||||
|
||||
const compactFormatter =
|
||||
(maximumFractionDigits = 2) =>
|
||||
(value: BigNumber) =>
|
||||
COMPACT_NUMBER_FORMAT(maximumFractionDigits).format(value.toNumber());
|
||||
|
||||
const valueFormatter = (noValueLabel: string) => (value: BigNumber) => {
|
||||
if (value.isNaN() || value.isZero()) {
|
||||
return noValueLabel;
|
||||
}
|
||||
return value.toString();
|
||||
};
|
||||
|
||||
export const dateFormatter = (value: string) => {
|
||||
try {
|
||||
return getDateFormat().format(new Date(value));
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
/* Helpers */
|
||||
|
||||
const Value = <T,>({
|
||||
data: { value, loading, error },
|
||||
formatter,
|
||||
}: {
|
||||
data: StatValue<T>;
|
||||
formatter: (value: T) => ReactNode;
|
||||
}) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<span className="p-[33px]">
|
||||
<Loader size="small" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return <span data-error={error.message}>-</span>;
|
||||
}
|
||||
|
||||
return formatter(value);
|
||||
};
|
||||
|
||||
/* Referrer tiles */
|
||||
|
||||
export const BaseCommissionTile = ({
|
||||
baseCommission,
|
||||
runningVolume,
|
||||
aggregationEpochs,
|
||||
}: {
|
||||
baseCommission: ReferrerStats['baseCommission'];
|
||||
runningVolume: ReferrerStats['runningVolume'];
|
||||
aggregationEpochs: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const runningVolumeDescription = compactFormatter(2)(runningVolume.value);
|
||||
const description = t(
|
||||
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
|
||||
{
|
||||
runningVolume: runningVolumeDescription,
|
||||
epochs: aggregationEpochs.toString(),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<StatTile
|
||||
title={t('Base commission rate')}
|
||||
description={description}
|
||||
testId="base-commission-rate"
|
||||
>
|
||||
<Value data={baseCommission} formatter={percentageFormatter} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const StakingMultiplierTile = ({
|
||||
multiplier,
|
||||
}: {
|
||||
multiplier: ReferrerStats['multiplier'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { stakeAvailable, isEligible } = useStakeAvailable();
|
||||
|
||||
const description = (
|
||||
<span
|
||||
className={classNames({
|
||||
'text-vega-red': !isEligible,
|
||||
})}
|
||||
>
|
||||
{t('{{amount}} $VEGA staked', {
|
||||
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<StatTile
|
||||
title={t('Staking multiplier')}
|
||||
description={description}
|
||||
testId="staking-multiplier"
|
||||
>
|
||||
<Value data={multiplier} formatter={valueFormatter(t('None'))} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const FinalCommissionTile = ({
|
||||
baseCommission,
|
||||
multiplier,
|
||||
finalCommission,
|
||||
}: {
|
||||
baseCommission: ReferrerStats['baseCommission'];
|
||||
multiplier: ReferrerStats['multiplier'];
|
||||
finalCommission: ReferrerStats['finalCommission'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const description =
|
||||
!baseCommission.loading && !finalCommission.loading && !multiplier.loading
|
||||
? `(${percentageFormatter(
|
||||
baseCommission.value
|
||||
)} × ${multiplier.value.toString()} = ${percentageFormatter(
|
||||
finalCommission.value
|
||||
)})`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<StatTile
|
||||
title={t('Final commission rate')}
|
||||
description={description}
|
||||
testId="final-commission-rate"
|
||||
>
|
||||
<Value data={finalCommission} formatter={percentageFormatter} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const VolumeTile = ({
|
||||
volume,
|
||||
aggregationEpochs,
|
||||
}: {
|
||||
volume: ReferrerStats['volume'];
|
||||
aggregationEpochs: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<StatTile
|
||||
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: aggregationEpochs,
|
||||
})}
|
||||
testId="my-volume"
|
||||
>
|
||||
<Value data={volume} formatter={compactFormatter(2)} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const TotalCommissionTile = ({
|
||||
totalCommission,
|
||||
aggregationEpochs,
|
||||
}: {
|
||||
totalCommission: ReferrerStats['totalCommission'];
|
||||
aggregationEpochs: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<StatTile
|
||||
testId="total-commission"
|
||||
title={
|
||||
<Trans
|
||||
i18nKey="totalCommission"
|
||||
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
||||
values={{
|
||||
count: aggregationEpochs,
|
||||
}}
|
||||
components={[
|
||||
<Tooltip
|
||||
key="0"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
<span>last 30 epochs</span>
|
||||
</Tooltip>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
description={<QUSDTooltip />}
|
||||
>
|
||||
<Value data={totalCommission} formatter={compactFormatter(0)} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const RefereesTile = ({
|
||||
referees,
|
||||
}: {
|
||||
referees: ReferrerStats['referees'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<StatTile title={t('Number of traders')} testId="number-of-traders">
|
||||
<Value data={referees} formatter={valueFormatter(t('None'))} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
/* Referee tiles */
|
||||
|
||||
export const BenefitTierTile = ({
|
||||
benefitTier,
|
||||
nextBenefitTier,
|
||||
}: {
|
||||
benefitTier: RefereeStats['benefitTier'];
|
||||
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const formatter = (value: BenefitTier | undefined) =>
|
||||
value?.tier || t('None');
|
||||
|
||||
const next = nextBenefitTier.value?.tier;
|
||||
|
||||
return (
|
||||
<StatTile
|
||||
title={t('Current tier')}
|
||||
testId="current-tier"
|
||||
description={
|
||||
next
|
||||
? t('(Next tier: {{nextTier}})', {
|
||||
nextTier: next,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Value<BenefitTier | undefined>
|
||||
data={benefitTier}
|
||||
formatter={formatter}
|
||||
/>
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const RunningVolumeTile = ({
|
||||
runningVolume,
|
||||
aggregationEpochs,
|
||||
}: {
|
||||
runningVolume: RefereeStats['runningVolume'];
|
||||
aggregationEpochs: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<StatTile
|
||||
title={t(
|
||||
'runningNotionalOverEpochs',
|
||||
'Combined volume (last {{count}} epochs)',
|
||||
{
|
||||
count: aggregationEpochs,
|
||||
}
|
||||
)}
|
||||
testId="combined-volume"
|
||||
>
|
||||
<Value data={runningVolume} formatter={compactFormatter(2)} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const DiscountTile = ({
|
||||
discountFactor,
|
||||
}: {
|
||||
discountFactor: RefereeStats['discountFactor'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<StatTile title={t('Discount')} testId="discount">
|
||||
<Value data={discountFactor} formatter={percentageFormatter} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const NextTierVolumeTile = ({
|
||||
runningVolume,
|
||||
nextBenefitTier,
|
||||
}: {
|
||||
runningVolume: RefereeStats['runningVolume'];
|
||||
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const data = {
|
||||
loading: runningVolume.loading || nextBenefitTier.loading,
|
||||
error: runningVolume.error || nextBenefitTier.error,
|
||||
value: [runningVolume.value, nextBenefitTier.value] as [
|
||||
BigNumber,
|
||||
BenefitTier | undefined
|
||||
],
|
||||
};
|
||||
|
||||
const formatter = ([runningVolume, nextBenefitTier]: [
|
||||
BigNumber,
|
||||
BenefitTier | undefined
|
||||
]) => {
|
||||
if (!nextBenefitTier) return '0';
|
||||
const volume = BigNumber(nextBenefitTier.minimumVolume).minus(
|
||||
runningVolume
|
||||
);
|
||||
if (volume.isNaN() || volume.isLessThan(0)) return '0';
|
||||
return compactFormatter(0)(volume);
|
||||
};
|
||||
|
||||
return (
|
||||
<StatTile title={t('Volume to next tier')} testId="vol-to-next-tier">
|
||||
<Value<[BigNumber, BenefitTier | undefined]>
|
||||
data={data}
|
||||
formatter={formatter}
|
||||
/>
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const EpochsTile = ({ epochs }: { epochs: RefereeStats['epochs'] }) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
|
||||
<Value data={epochs} formatter={valueFormatter(t('None'))} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const NextTierEpochsTile = ({
|
||||
epochs,
|
||||
nextBenefitTier,
|
||||
}: {
|
||||
epochs: RefereeStats['epochs'];
|
||||
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const data = {
|
||||
value: [epochs.value, nextBenefitTier.value] as [
|
||||
BigNumber,
|
||||
BenefitTier | undefined
|
||||
],
|
||||
loading: epochs.loading || nextBenefitTier.loading,
|
||||
error: epochs.error || nextBenefitTier.error,
|
||||
};
|
||||
|
||||
const formatter = ([epochs, nextBenefitTier]: [
|
||||
BigNumber,
|
||||
BenefitTier | undefined
|
||||
]) => {
|
||||
if (!nextBenefitTier) return '-';
|
||||
const value = BigNumber(nextBenefitTier.epochs).minus(epochs);
|
||||
if (value.isLessThan(0)) {
|
||||
return '0';
|
||||
}
|
||||
return value.toString(10);
|
||||
};
|
||||
|
||||
return (
|
||||
<StatTile title={t('Epochs to next tier')} testId="epochs-to-next-tier">
|
||||
<Value data={data} formatter={formatter} />
|
||||
</StatTile>
|
||||
);
|
||||
};
|
||||
|
||||
/* Additional settings */
|
||||
|
||||
/**
|
||||
* A list for tiles that should be replaced with `NoProgramTile`
|
||||
* when the referral program is not set.
|
||||
*/
|
||||
const NO_PROGRAM_TILES = {
|
||||
[BaseCommissionTile.name]: 'Base commission rate',
|
||||
[StakingMultiplierTile.name]: 'Staking multiplier',
|
||||
[FinalCommissionTile.name]: 'Final commission rate',
|
||||
[VolumeTile.name]: 'My volume',
|
||||
[BenefitTierTile.name]: 'Current tier',
|
||||
[DiscountTile.name]: 'Discount',
|
||||
[RunningVolumeTile.name]: 'Combined volume',
|
||||
[NextTierEpochsTile.name]: 'Epochs to next tier',
|
||||
[NextTierVolumeTile.name]: 'Volume to next tier',
|
||||
};
|
||||
|
||||
export const NoProgramTileFor = ({ tile }: { tile: string }) => {
|
||||
const t = useT();
|
||||
if (Object.keys(NO_PROGRAM_TILES).includes(tile)) {
|
||||
return <NoProgramTile title={t(NO_PROGRAM_TILES[tile])} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Teams */
|
||||
|
||||
export const TeamTile = ({ teamId }: { teamId?: string }) => {
|
||||
const { team, members } = useTeam(teamId);
|
||||
const { data: games } = useGames(teamId);
|
||||
|
||||
if (!team) return null;
|
||||
|
||||
return (
|
||||
<Tile className="flex gap-3 lg:gap-4">
|
||||
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
||||
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
|
||||
<TeamStats
|
||||
members={members}
|
||||
games={areTeamGames(games) ? games : undefined}
|
||||
/>
|
||||
</div>
|
||||
</Tile>
|
||||
);
|
||||
};
|
||||
@@ -6,12 +6,7 @@ export const SUPPORTED_INTERVALS = [
|
||||
Interval.INTERVAL_I1M,
|
||||
Interval.INTERVAL_I5M,
|
||||
Interval.INTERVAL_I15M,
|
||||
Interval.INTERVAL_I30M,
|
||||
Interval.INTERVAL_I1H,
|
||||
Interval.INTERVAL_I4H,
|
||||
Interval.INTERVAL_I6H,
|
||||
Interval.INTERVAL_I8H,
|
||||
Interval.INTERVAL_I12H,
|
||||
Interval.INTERVAL_I1D,
|
||||
Interval.INTERVAL_I7D,
|
||||
] as const;
|
||||
|
||||
@@ -50,8 +50,6 @@ def truncate_middle(market_id, start=6, end=4):
|
||||
def change_keys(page: Page, vega: VegaServiceNull, key_name):
|
||||
page.get_by_test_id("manage-vega-wallet").click()
|
||||
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
|
||||
page.click(
|
||||
f'data-testid=key-{vega.wallet.public_key(key_name)} >> .inline-flex')
|
||||
page.reload()
|
||||
|
||||
|
||||
|
||||
@@ -37,16 +37,19 @@ def validate_info_section(page: Page, fields: [[str, str]]):
|
||||
for rowNumber, field in enumerate(fields):
|
||||
name, value = field
|
||||
expect(
|
||||
page.get_by_test_id("key-value-table-row").nth(rowNumber).locator("dt")
|
||||
page.get_by_test_id(
|
||||
"key-value-table-row").nth(rowNumber).locator("dt")
|
||||
).to_contain_text(name)
|
||||
expect(
|
||||
page.get_by_test_id("key-value-table-row").nth(rowNumber).locator("dd")
|
||||
page.get_by_test_id(
|
||||
"key-value-table-row").nth(rowNumber).locator("dd")
|
||||
).to_contain_text(value)
|
||||
|
||||
|
||||
def test_market_info_current_fees(page: Page):
|
||||
# 6002-MDET-101
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Current fees").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Current fees").click()
|
||||
fields = [
|
||||
["Maker Fee", "10%"],
|
||||
["Infrastructure Fee", "0.05%"],
|
||||
@@ -58,7 +61,8 @@ def test_market_info_current_fees(page: Page):
|
||||
|
||||
def test_market_info_market_price(page: Page):
|
||||
# 6002-MDET-102
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Market price").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Market price").click()
|
||||
fields = [
|
||||
["Mark Price", "107.50"],
|
||||
["Best Bid Price", "101.50"],
|
||||
@@ -67,10 +71,11 @@ def test_market_info_market_price(page: Page):
|
||||
]
|
||||
validate_info_section(page, fields)
|
||||
|
||||
#TODO: remove skip once volume is fixed
|
||||
""" def test_market_info_market_volume(page: Page):
|
||||
|
||||
def test_market_info_market_volume(page: Page):
|
||||
# 6002-MDET-103
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Market volume").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Market volume").click()
|
||||
fields = [
|
||||
["24 Hour Volume", "0 (0 )"],
|
||||
["Open Interest", "1"],
|
||||
@@ -79,13 +84,12 @@ def test_market_info_market_price(page: Page):
|
||||
["Best Static Bid Volume", "1"],
|
||||
["Best Static Offer Volume", "1"],
|
||||
]
|
||||
validate_info_section(page, fields) """
|
||||
validate_info_section(page, fields)
|
||||
|
||||
|
||||
def test_market_info_liquidation_strategy(page: Page):
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Liquidation strategy"
|
||||
).click()
|
||||
"Liquidation strategy").click()
|
||||
fields = [
|
||||
["Disposal Fraction", "1"],
|
||||
["Disposal Time Step", "1"],
|
||||
@@ -97,14 +101,16 @@ def test_market_info_liquidation_strategy(page: Page):
|
||||
|
||||
def test_market_info_liquidation(page: Page):
|
||||
# 6002-MDET-104
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Liquidations").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Liquidations").click()
|
||||
fields = [["Insurance Pool Balance", "0.00 tDAI"]]
|
||||
validate_info_section(page, fields)
|
||||
|
||||
|
||||
def test_market_info_key_details(page: Page, vega: VegaServiceNull):
|
||||
# 6002-MDET-201
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Key details").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Key details").click()
|
||||
market_id = vega.find_market_id("BTC:DAI_2023")
|
||||
short_market_id = market_id[:6] + "…" + market_id[-4:]
|
||||
fields = [
|
||||
@@ -150,7 +156,8 @@ def test_market_info_oracle(page: Page):
|
||||
|
||||
def test_market_info_settlement_asset(page: Page, vega: VegaServiceNull):
|
||||
# 6002-MDET-206
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Settlement asset").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Settlement asset").click()
|
||||
tdai_id = vega.find_asset_id("tDAI")
|
||||
tdai_id_short = tdai_id[:6] + "…" + tdai_id[-4:]
|
||||
fields = [
|
||||
@@ -204,7 +211,8 @@ def test_market_info_margin_scaling_factors(page: Page):
|
||||
|
||||
def test_market_info_risk_factors(page: Page):
|
||||
# 6002-MDET-210
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Risk factors").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Risk factors").click()
|
||||
fields = [
|
||||
["Long", "0.05153"],
|
||||
["Short", "0.05422"],
|
||||
@@ -224,7 +232,8 @@ def test_market_info_price_monitoring_bounds(page: Page):
|
||||
expect(page.locator("p.col-span-1").nth(0)).to_contain_text(
|
||||
"99.9999% probability price bounds"
|
||||
)
|
||||
expect(page.locator("p.col-span-1").nth(1)).to_contain_text("Within 86,400 seconds")
|
||||
expect(page.locator("p.col-span-1").nth(1)
|
||||
).to_contain_text("Within 86,400 seconds")
|
||||
fields = [
|
||||
["Highest Price", "138.66685 BTC"],
|
||||
["Lowest Price", "83.11038 BTC"],
|
||||
@@ -245,7 +254,7 @@ def test_market_info_liquidity_monitoring_parameters(page: Page):
|
||||
|
||||
|
||||
# Liquidity resolves to 3 results
|
||||
def test_market_info_liquidity(page: Page):
|
||||
def test_market_info_liquidit(page: Page):
|
||||
# 6002-MDET-213
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Liquidity", exact=True
|
||||
@@ -274,14 +283,17 @@ def test_market_info_proposal(page: Page, vega: VegaServiceNull):
|
||||
# 6002-MDET-301
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Proposal").click()
|
||||
first_link = (
|
||||
page.get_by_test_id("accordion-content").get_by_test_id("external-link").first
|
||||
page.get_by_test_id(
|
||||
"accordion-content").get_by_test_id("external-link").first
|
||||
)
|
||||
second_link = (
|
||||
page.get_by_test_id("accordion-content").get_by_test_id("external-link").nth(1)
|
||||
page.get_by_test_id(
|
||||
"accordion-content").get_by_test_id("external-link").nth(1)
|
||||
)
|
||||
expect(first_link).to_have_text("View governance proposal")
|
||||
expect(first_link).to_have_attribute(
|
||||
"href", re.compile(rf'(\/proposals\/{vega.find_market_id("BTC:DAI_2023")})')
|
||||
"href", re.compile(
|
||||
rf'(\/proposals\/{vega.find_market_id("BTC:DAI_2023")})')
|
||||
)
|
||||
expect(second_link).to_have_text("Propose a change to market")
|
||||
|
||||
@@ -292,10 +304,12 @@ def test_market_info_proposal(page: Page, vega: VegaServiceNull):
|
||||
|
||||
|
||||
def test_market_info_succession_line(page: Page, vega: VegaServiceNull):
|
||||
page.get_by_test_id(market_title_test_id).get_by_text("Succession line").click()
|
||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||
"Succession line").click()
|
||||
market_id = vega.find_market_id("BTC:DAI_2023")
|
||||
succession_line = page.get_by_test_id("succession-line-item")
|
||||
expect(succession_line.get_by_test_id("external-link")).to_have_text("BTC:DAI_2023")
|
||||
expect(succession_line.get_by_test_id(
|
||||
"external-link")).to_have_text("BTC:DAI_2023")
|
||||
expect(succession_line.get_by_test_id("external-link")).to_have_attribute(
|
||||
"href", re.compile(rf"(\/proposals\/{market_id})")
|
||||
)
|
||||
|
||||
@@ -97,13 +97,10 @@ def test_banners(vega: VegaServiceNull, page: Page):
|
||||
settlement_price=100,
|
||||
market_id=parent_market_id,
|
||||
)
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
successor_name = "successor market name"
|
||||
propose_successor(vega, parent_market_id, tdai_id, successor_name)
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
page.reload()
|
||||
|
||||
# Check that the banners notifying about the successor proposal and market has been settled are shown
|
||||
banner = page.get_by_test_id(market_banner)
|
||||
expect(banner).to_be_attached()
|
||||
|
||||
@@ -299,10 +299,10 @@ def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||
|
||||
|
||||
def test_game_card(competitions_page: Page):
|
||||
expect(competitions_page.get_by_test_id("active-rewards-card")).to_have_count(1)
|
||||
expect(competitions_page.get_by_test_id("active-rewards-card")).to_have_count(2)
|
||||
game_1 = competitions_page.get_by_test_id("active-rewards-card").first
|
||||
expect(game_1).to_be_visible()
|
||||
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Team")
|
||||
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Individual")
|
||||
expect(game_1.get_by_test_id("locked-for")).to_have_text("1 epoch")
|
||||
expect(game_1.get_by_test_id("reward-value")).to_have_text("100.00")
|
||||
expect(game_1.get_by_test_id("reward-asset")).to_have_text("VEGA")
|
||||
@@ -311,7 +311,7 @@ def test_game_card(competitions_page: Page):
|
||||
"Price maker fees paid • tDAI"
|
||||
)
|
||||
expect(game_1.get_by_test_id("assessed-over")).to_have_text("15 epochs")
|
||||
expect(game_1.get_by_test_id("scope")).to_have_text("All teams")
|
||||
expect(game_1.get_by_test_id("scope")).to_have_text("In team")
|
||||
expect(game_1.get_by_test_id("staking-requirement")).to_have_text("0.00")
|
||||
expect(game_1.get_by_test_id("average-position")).to_have_text("0.00")
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@ describe('isScopedToTeams', () => {
|
||||
undefined,
|
||||
makeDispatchStrategy(
|
||||
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
|
||||
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM // individual in teams but not a team game
|
||||
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM // individual in teams
|
||||
),
|
||||
'RecurringTransfer'
|
||||
),
|
||||
false,
|
||||
true,
|
||||
],
|
||||
[
|
||||
makeReward(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TransferStatus,
|
||||
type DispatchStrategy,
|
||||
EntityScope,
|
||||
IndividualScope,
|
||||
MarketState,
|
||||
AccountType,
|
||||
} from '@vegaprotocol/types';
|
||||
@@ -74,11 +75,20 @@ export const isActiveReward = (node: RewardTransfer, currentEpoch: number) => {
|
||||
|
||||
/**
|
||||
* Checks if given reward (transfer) is scoped to teams.
|
||||
*
|
||||
* A reward is scoped to teams if it's entity scope is set to teams or
|
||||
* if the scope is set to individuals but the individuals are in a team.
|
||||
*/
|
||||
export const isScopedToTeams = (node: EnrichedRewardTransfer) =>
|
||||
// scoped to teams
|
||||
node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_TEAMS;
|
||||
EntityScope.ENTITY_SCOPE_TEAMS ||
|
||||
// or to individuals
|
||||
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
|
||||
// but they have to be in a team
|
||||
node.transfer.kind.dispatchStrategy?.individualScope ===
|
||||
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM);
|
||||
|
||||
/** Retrieves rewards (transfers) */
|
||||
export const useRewards = ({
|
||||
|
||||
@@ -6,12 +6,7 @@ export const PENNANT_INTERVAL_MAP = {
|
||||
[Interval.INTERVAL_I1M]: PennantInterval.I1M,
|
||||
[Interval.INTERVAL_I5M]: PennantInterval.I5M,
|
||||
[Interval.INTERVAL_I15M]: PennantInterval.I15M,
|
||||
[Interval.INTERVAL_I30M]: PennantInterval.I30M,
|
||||
[Interval.INTERVAL_I1H]: PennantInterval.I1H,
|
||||
[Interval.INTERVAL_I4H]: PennantInterval.I4H,
|
||||
[Interval.INTERVAL_I6H]: PennantInterval.I6H,
|
||||
[Interval.INTERVAL_I8H]: PennantInterval.I8H,
|
||||
[Interval.INTERVAL_I12H]: PennantInterval.I12H,
|
||||
[Interval.INTERVAL_I1D]: PennantInterval.I1D,
|
||||
[Interval.INTERVAL_I7D]: PennantInterval.I7D,
|
||||
} as const;
|
||||
|
||||
@@ -34,28 +34,18 @@ const INTERVAL_TO_PENNANT_MAP = {
|
||||
[PennantInterval.I1M]: Schema.Interval.INTERVAL_I1M,
|
||||
[PennantInterval.I5M]: Schema.Interval.INTERVAL_I5M,
|
||||
[PennantInterval.I15M]: Schema.Interval.INTERVAL_I15M,
|
||||
[PennantInterval.I30M]: Schema.Interval.INTERVAL_I30M,
|
||||
[PennantInterval.I1H]: Schema.Interval.INTERVAL_I1H,
|
||||
[PennantInterval.I4H]: Schema.Interval.INTERVAL_I4H,
|
||||
[PennantInterval.I6H]: Schema.Interval.INTERVAL_I6H,
|
||||
[PennantInterval.I8H]: Schema.Interval.INTERVAL_I8H,
|
||||
[PennantInterval.I12H]: Schema.Interval.INTERVAL_I12H,
|
||||
[PennantInterval.I1D]: Schema.Interval.INTERVAL_I1D,
|
||||
[PennantInterval.I7D]: Schema.Interval.INTERVAL_I7D,
|
||||
};
|
||||
|
||||
const defaultConfig = {
|
||||
decimalPlaces: 5,
|
||||
supportedIntervals: [
|
||||
PennantInterval.I7D,
|
||||
PennantInterval.I1D,
|
||||
PennantInterval.I12H,
|
||||
PennantInterval.I8H,
|
||||
PennantInterval.I6H,
|
||||
PennantInterval.I4H,
|
||||
PennantInterval.I1H,
|
||||
PennantInterval.I15M,
|
||||
PennantInterval.I30M,
|
||||
PennantInterval.I5M,
|
||||
PennantInterval.I1M,
|
||||
],
|
||||
@@ -147,15 +137,10 @@ export class VegaDataSource implements DataSource {
|
||||
decimalPlaces: this._decimalPlaces,
|
||||
positionDecimalPlaces: this._positionDecimalPlaces,
|
||||
supportedIntervals: [
|
||||
PennantInterval.I7D,
|
||||
PennantInterval.I1D,
|
||||
PennantInterval.I12H,
|
||||
PennantInterval.I8H,
|
||||
PennantInterval.I6H,
|
||||
PennantInterval.I4H,
|
||||
PennantInterval.I1H,
|
||||
PennantInterval.I15M,
|
||||
PennantInterval.I30M,
|
||||
PennantInterval.I5M,
|
||||
PennantInterval.I1M,
|
||||
],
|
||||
@@ -270,10 +255,6 @@ const getDuration = (
|
||||
multiplier: number
|
||||
): Duration => {
|
||||
switch (interval) {
|
||||
case 'I7D':
|
||||
return {
|
||||
days: 7 * multiplier,
|
||||
};
|
||||
case 'I1D':
|
||||
return {
|
||||
days: 1 * multiplier,
|
||||
@@ -290,30 +271,14 @@ const getDuration = (
|
||||
return {
|
||||
minutes: 5 * multiplier,
|
||||
};
|
||||
case 'I4H':
|
||||
return {
|
||||
hours: 4 * multiplier,
|
||||
};
|
||||
case 'I6H':
|
||||
return {
|
||||
hours: 6 * multiplier,
|
||||
};
|
||||
case 'I8H':
|
||||
return {
|
||||
hours: 8 * multiplier,
|
||||
};
|
||||
case 'I12H':
|
||||
return {
|
||||
hours: 12 * multiplier,
|
||||
};
|
||||
case 'I15M':
|
||||
return {
|
||||
minutes: 15 * multiplier,
|
||||
};
|
||||
case 'I30M':
|
||||
return {
|
||||
minutes: 30 * multiplier,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -323,24 +288,14 @@ const getDifference = (
|
||||
dateRight: Date
|
||||
): number => {
|
||||
switch (interval) {
|
||||
case 'I7D':
|
||||
return differenceInDays(dateRight, dateLeft) / 7;
|
||||
case 'I1D':
|
||||
return differenceInDays(dateRight, dateLeft);
|
||||
case 'I12H':
|
||||
return differenceInHours(dateRight, dateLeft) / 12;
|
||||
case 'I8H':
|
||||
return differenceInHours(dateRight, dateLeft) / 8;
|
||||
case 'I6H':
|
||||
return differenceInHours(dateRight, dateLeft) / 6;
|
||||
case 'I4H':
|
||||
return differenceInHours(dateRight, dateLeft) / 4;
|
||||
case 'I1H':
|
||||
return differenceInHours(dateRight, dateLeft);
|
||||
case 'I15M':
|
||||
return differenceInMinutes(dateRight, dateLeft) / 15;
|
||||
case 'I30M':
|
||||
return differenceInMinutes(dateRight, dateLeft) / 30;
|
||||
case 'I5M':
|
||||
return differenceInMinutes(dateRight, dateLeft) / 5;
|
||||
case 'I1M':
|
||||
|
||||
@@ -73,7 +73,7 @@ export const DealTicketContainer = ({
|
||||
market={market}
|
||||
marketPrice={marketPrice}
|
||||
marketData={marketData}
|
||||
submit={(transaction) => create(transaction)}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import { Controller, type Control } from 'react-hook-form';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import { toDecimal, useValidateAmount } from '@vegaprotocol/utils';
|
||||
import {
|
||||
TradingFormGroup,
|
||||
TradingInputError,
|
||||
Tooltip,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
Pill,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export interface DealTicketPriceTakeProfitStopLossProps {
|
||||
control: Control<OrderFormValues>;
|
||||
market: Market;
|
||||
takeProfitError?: string;
|
||||
stopLossError?: string;
|
||||
quoteName?: string;
|
||||
}
|
||||
|
||||
export const DealTicketPriceTakeProfitStopLoss = ({
|
||||
control,
|
||||
market,
|
||||
takeProfitError,
|
||||
stopLossError,
|
||||
quoteName,
|
||||
}: DealTicketPriceTakeProfitStopLossProps) => {
|
||||
const t = useT();
|
||||
const validateAmount = useValidateAmount();
|
||||
const priceStep = toDecimal(market?.decimalPlaces);
|
||||
|
||||
const renderTakeProfitError = () => {
|
||||
if (takeProfitError) {
|
||||
return (
|
||||
<TradingInputError testId="deal-ticket-take-profit-error-message">
|
||||
{takeProfitError}
|
||||
</TradingInputError>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderStopLossError = () => {
|
||||
if (stopLossError) {
|
||||
return (
|
||||
<TradingInputError testId="deal-stop-loss-error-message">
|
||||
{stopLossError}
|
||||
</TradingInputError>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex-1">
|
||||
<TradingFormGroup
|
||||
label={
|
||||
<Tooltip
|
||||
description={<div>{t('The price for take profit.')}</div>}
|
||||
>
|
||||
<span className="text-xs">{t('Take profit')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
labelFor="input-order-take-profit"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="takeProfit"
|
||||
control={control}
|
||||
rules={{
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t(
|
||||
'Take profit price cannot be lower than {{priceStep}}',
|
||||
{
|
||||
priceStep,
|
||||
}
|
||||
),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'takeProfit'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-2">
|
||||
<FormGroup
|
||||
labelFor="input-price-take-profit"
|
||||
label={''}
|
||||
compact
|
||||
>
|
||||
<Input
|
||||
id="input-price-take-profit"
|
||||
appendElement={<Pill size="xs">{quoteName}</Pill>}
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price-take-profit"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId="deal-ticket-error-message-price-take-profit">
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TradingFormGroup
|
||||
label={
|
||||
<Tooltip description={<div>{t('The price for stop loss.')}</div>}>
|
||||
<span className="text-xs">{t('Stop loss')}</span>
|
||||
</Tooltip>
|
||||
}
|
||||
labelFor="input-order-stop-loss"
|
||||
className="!mb-1"
|
||||
>
|
||||
<Controller
|
||||
name="stopLoss"
|
||||
control={control}
|
||||
rules={{
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than {{priceStep}}', {
|
||||
priceStep,
|
||||
}),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'stopLoss'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-2">
|
||||
<FormGroup
|
||||
labelFor="input-price-stop-loss"
|
||||
label={''}
|
||||
compact
|
||||
>
|
||||
<Input
|
||||
id="input-price-stop-loss"
|
||||
appendElement={<Pill size="xs">{quoteName}</Pill>}
|
||||
className="w-full"
|
||||
type="number"
|
||||
step={priceStep}
|
||||
data-testid="order-price-stop-loss"
|
||||
onWheel={(e) => e.currentTarget.blur()}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId="deal-ticket-error-message-price-stop-loss">
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
</div>
|
||||
</div>
|
||||
{renderTakeProfitError()}
|
||||
{renderStopLossError()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1003,7 +1003,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
name="oco"
|
||||
label={
|
||||
<Tooltip
|
||||
description={<span>{t('One cancels the other')}</span>}
|
||||
description={<span>{t('One cancels another')}</span>}
|
||||
>
|
||||
<>{t('OCO')}</>
|
||||
</Tooltip>
|
||||
|
||||
@@ -8,12 +8,9 @@ import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import { type OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||
import { type Transaction } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
mapFormValuesToOrderSubmission,
|
||||
mapFormValuesToTakeProfitAndStopLoss,
|
||||
} from '../../utils/map-form-values-to-submission';
|
||||
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
|
||||
import {
|
||||
TradingInput as Input,
|
||||
TradingCheckbox as Checkbox,
|
||||
@@ -80,8 +77,6 @@ import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
import { DealTicketPriceTakeProfitStopLoss } from './deal-ticket-price-tp-sl';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
|
||||
export const REDUCE_ONLY_TOOLTIP =
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||
@@ -91,7 +86,7 @@ export interface DealTicketProps {
|
||||
marketData: StaticMarketData;
|
||||
marketPrice?: string | null;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
submit: (order: Transaction) => void;
|
||||
submit: (order: OrderSubmission) => void;
|
||||
onDeposit: (assetId: string) => void;
|
||||
}
|
||||
|
||||
@@ -189,7 +184,6 @@ export const DealTicket = ({
|
||||
const rawSize = watch('size');
|
||||
const rawPrice = watch('price');
|
||||
const iceberg = watch('iceberg');
|
||||
const tpSl = watch('tpSl');
|
||||
const peakSize = watch('peakSize');
|
||||
const expiresAt = watch('expiresAt');
|
||||
const postOnly = watch('postOnly');
|
||||
@@ -388,28 +382,17 @@ export const DealTicket = ({
|
||||
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||
return;
|
||||
}
|
||||
if (formValues.tpSl) {
|
||||
const reference = `${pubKey}-${now}-${uniqueId()}`;
|
||||
const batchMarketInstructions = mapFormValuesToTakeProfitAndStopLoss(
|
||||
formValues,
|
||||
market,
|
||||
reference
|
||||
);
|
||||
submit({
|
||||
batchMarketInstructions,
|
||||
});
|
||||
} else {
|
||||
const orderSubmission = mapFormValuesToOrderSubmission(
|
||||
submit(
|
||||
mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
);
|
||||
submit({ orderSubmission });
|
||||
}
|
||||
)
|
||||
);
|
||||
lastSubmitTime.current = now;
|
||||
},
|
||||
[market, pubKey, submit]
|
||||
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
|
||||
);
|
||||
useController({
|
||||
name: 'type',
|
||||
@@ -691,63 +674,40 @@ export const DealTicket = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
{isLimitType && (
|
||||
<>
|
||||
<div className="flex justify-between gap-2 pb-2">
|
||||
{isLimitType && (
|
||||
<Controller
|
||||
name="iceberg"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Tooltip
|
||||
description={
|
||||
<p>
|
||||
{t(
|
||||
'ICEBERG_TOOLTIP',
|
||||
'Trade only a fraction of the order size at once. After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away. For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each. Note that the full volume of the order is not hidden and is still reflected in the order book.'
|
||||
)}{' '}
|
||||
<ExternalLink href={DocsLinks?.ICEBERG_ORDERS}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>{' '}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Checkbox
|
||||
name="iceberg"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={disableIcebergCheckbox}
|
||||
label={t('Iceberg')}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="tpSl"
|
||||
name="iceberg"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Tooltip
|
||||
description={
|
||||
<p>{t('TP_SL_TOOLTIP', 'Take profit / Stop loss')}</p>
|
||||
<p>
|
||||
{t(
|
||||
'ICEBERG_TOOLTIP',
|
||||
'Trade only a fraction of the order size at once. After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away. For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each. Note that the full volume of the order is not hidden and is still reflected in the order book.'
|
||||
)}{' '}
|
||||
<ExternalLink href={DocsLinks?.ICEBERG_ORDERS}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>{' '}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Checkbox
|
||||
name="tpSl"
|
||||
name="iceberg"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={false}
|
||||
label={t('TP / SL')}
|
||||
disabled={disableIcebergCheckbox}
|
||||
label={t('Iceberg')}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isLimitType && iceberg && (
|
||||
{iceberg && (
|
||||
<DealTicketSizeIceberg
|
||||
market={market}
|
||||
peakSizeError={errors.peakSize?.message}
|
||||
@@ -757,18 +717,8 @@ export const DealTicket = ({
|
||||
peakSize={peakSize}
|
||||
/>
|
||||
)}
|
||||
{tpSl && (
|
||||
<DealTicketPriceTakeProfitStopLoss
|
||||
market={market}
|
||||
takeProfitError={errors.takeProfit?.message}
|
||||
stopLossError={errors.stopLoss?.message}
|
||||
control={control}
|
||||
quoteName={quoteName}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
)}
|
||||
<SummaryMessage
|
||||
error={summaryError}
|
||||
asset={asset}
|
||||
|
||||
@@ -53,9 +53,6 @@ export type OrderFormValues = {
|
||||
iceberg?: boolean;
|
||||
peakSize?: string;
|
||||
minimumVisibleSize?: string;
|
||||
tpSl?: boolean;
|
||||
takeProfit?: string;
|
||||
stopLoss?: string;
|
||||
};
|
||||
|
||||
type UpdateOrder = (marketId: string, values: Partial<OrderFormValues>) => void;
|
||||
|
||||
@@ -10,16 +10,13 @@ import type {
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { isPersistentOrder } from './time-in-force-persistence';
|
||||
import { type MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||
|
||||
export const mapFormValuesToOrderSubmission = (
|
||||
order: OrderFormValues,
|
||||
marketId: string,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number,
|
||||
reference?: string
|
||||
positionDecimalPlaces: number
|
||||
): OrderSubmission => ({
|
||||
reference,
|
||||
marketId: marketId,
|
||||
type: order.type,
|
||||
side: order.side,
|
||||
@@ -84,8 +81,7 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
data: StopOrderFormValues,
|
||||
marketId: string,
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number,
|
||||
reference?: string
|
||||
positionDecimalPlaces: number
|
||||
): StopOrdersSubmission => {
|
||||
const submission: StopOrdersSubmission = {};
|
||||
const stopOrderSetup: StopOrderSetup = {
|
||||
@@ -100,8 +96,7 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
},
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
reference
|
||||
positionDecimalPlaces
|
||||
),
|
||||
};
|
||||
setTrigger(
|
||||
@@ -125,8 +120,7 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
},
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
reference
|
||||
positionDecimalPlaces
|
||||
),
|
||||
};
|
||||
setTrigger(
|
||||
@@ -165,125 +159,3 @@ export const mapFormValuesToStopOrdersSubmission = (
|
||||
|
||||
return submission;
|
||||
};
|
||||
|
||||
export const mapFormValuesToTakeProfitAndStopLoss = (
|
||||
formValues: OrderFormValues,
|
||||
market: MarketFieldsFragment,
|
||||
reference: string
|
||||
) => {
|
||||
const orderSubmission = mapFormValuesToOrderSubmission(
|
||||
formValues,
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
|
||||
const oppositeSide =
|
||||
formValues.side === Schema.Side.SIDE_BUY
|
||||
? Schema.Side.SIDE_SELL
|
||||
: Schema.Side.SIDE_BUY;
|
||||
// For direction it needs to be implied
|
||||
// If position is LONG (BUY)
|
||||
// TP is SHORT and trigger is RISES ABOVE
|
||||
// If position is SHORT
|
||||
// TP is LONG and trigger is FALLS BELOW
|
||||
const takeProfitTriggerDirection =
|
||||
formValues.side === Schema.Side.SIDE_BUY
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW;
|
||||
// For direction it needs to be implied
|
||||
// If position is LONG (BUY)
|
||||
// SL is SHORT and trigger is FALLS BELOW
|
||||
// If position is SHORT
|
||||
// SL is LONG and trigger is RISES ABOVE
|
||||
const stopLossTriggerDirection =
|
||||
formValues.side === Schema.Side.SIDE_BUY
|
||||
? Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||
: Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE;
|
||||
|
||||
const stopOrdersSubmission = [];
|
||||
|
||||
// if there are both take profit and stop loss then the stop order needs to be OCO
|
||||
if (formValues.takeProfit && formValues.stopLoss) {
|
||||
const ocoStopOrderSubmission = mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
triggerPrice: formValues.stopLoss,
|
||||
ocoTriggerPrice: formValues.takeProfit,
|
||||
price: formValues.stopLoss,
|
||||
triggerDirection: stopLossTriggerDirection,
|
||||
triggerType: 'price',
|
||||
side: oppositeSide,
|
||||
expire: false,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
oco: true,
|
||||
ocoPrice: formValues.takeProfit,
|
||||
ocoTriggerType: 'price',
|
||||
ocoType: Schema.OrderType.TYPE_MARKET,
|
||||
ocoSize: formValues.size,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
stopOrdersSubmission.push(ocoStopOrderSubmission);
|
||||
} else if (formValues.takeProfit) {
|
||||
const takeProfitStopOrderSubmission = mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
price: formValues.takeProfit,
|
||||
triggerDirection: takeProfitTriggerDirection,
|
||||
triggerType: 'price',
|
||||
triggerPrice: formValues.takeProfit,
|
||||
side: oppositeSide,
|
||||
expire: false,
|
||||
ocoTriggerType: 'price',
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
oco: false,
|
||||
ocoType: Schema.OrderType.TYPE_MARKET,
|
||||
ocoSize: formValues.size,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
stopOrdersSubmission.push(takeProfitStopOrderSubmission);
|
||||
} else if (formValues.stopLoss) {
|
||||
const stopLossStopOrderSubmission = mapFormValuesToStopOrdersSubmission(
|
||||
{
|
||||
...formValues,
|
||||
triggerPrice: formValues.stopLoss,
|
||||
price: formValues.stopLoss,
|
||||
triggerDirection: stopLossTriggerDirection,
|
||||
triggerType: 'price',
|
||||
side: oppositeSide,
|
||||
expire: false,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
oco: false,
|
||||
ocoTriggerType: 'price',
|
||||
ocoType: Schema.OrderType.TYPE_MARKET,
|
||||
ocoSize: formValues.size,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
},
|
||||
market.id,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
reference
|
||||
);
|
||||
stopOrdersSubmission.push(stopLossStopOrderSubmission);
|
||||
}
|
||||
|
||||
const batchMarketInstructions = {
|
||||
submissions: [orderSubmission],
|
||||
stopOrdersSubmission,
|
||||
};
|
||||
return batchMarketInstructions;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import type {
|
||||
OrderSubmissionBody,
|
||||
StopOrdersSubmission,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
mapFormValuesToOrderSubmission,
|
||||
mapFormValuesToTakeProfitAndStopLoss,
|
||||
} from './map-form-values-to-submission';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import type { OrderFormValues } from '../hooks';
|
||||
import { type MarketFieldsFragment } from '@vegaprotocol/markets';
|
||||
|
||||
describe('mapFormValuesToOrderSubmission', () => {
|
||||
it('sets and formats price only for limit orders', () => {
|
||||
@@ -193,232 +186,3 @@ describe('mapFormValuesToOrderSubmission', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const mockMarket: MarketFieldsFragment = {
|
||||
__typename: 'Market',
|
||||
id: 'marketId',
|
||||
decimalPlaces: 1,
|
||||
positionDecimalPlaces: 4,
|
||||
state: Schema.MarketState.STATE_ACTIVE,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
} as MarketFieldsFragment;
|
||||
|
||||
const orderFormValues: OrderFormValues = {
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
size: '1',
|
||||
price: '66300',
|
||||
postOnly: false,
|
||||
reduceOnly: false,
|
||||
tpSl: true,
|
||||
takeProfit: '70000',
|
||||
stopLoss: '60000',
|
||||
};
|
||||
|
||||
describe('mapFormValuesToTakeProfitAndStopLoss', () => {
|
||||
it('creates batch market instructions for a normal order created with TP and SL', () => {
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValues,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [
|
||||
{
|
||||
fallsBelow: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '600000',
|
||||
},
|
||||
risesAbove: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '700000',
|
||||
},
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates batch market instructions for a normal order created without TP and SL', () => {
|
||||
// Create order form values without TP and SL
|
||||
const orderFormValuesWithoutTPSL = { ...orderFormValues };
|
||||
delete orderFormValuesWithoutTPSL.takeProfit;
|
||||
delete orderFormValuesWithoutTPSL.stopLoss;
|
||||
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValuesWithoutTPSL,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
// Expected result when TP and SL are not provided
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates batch market instructions for a normal order created with TP only', () => {
|
||||
// Create order form values with TP only
|
||||
const orderFormValuesWithTP = { ...orderFormValues };
|
||||
orderFormValuesWithTP.stopLoss = undefined;
|
||||
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValuesWithTP,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
// Expected result when only TP is provided
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [
|
||||
{
|
||||
risesAbove: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '700000',
|
||||
},
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('creates batch market instructions for a normal order created with SL only', () => {
|
||||
// Create order form values with SL only
|
||||
const orderFormValuesWithSL = { ...orderFormValues };
|
||||
orderFormValuesWithSL.takeProfit = undefined;
|
||||
|
||||
const result = mapFormValuesToTakeProfitAndStopLoss(
|
||||
orderFormValuesWithSL,
|
||||
mockMarket,
|
||||
'reference'
|
||||
);
|
||||
|
||||
// Expected result when only SL is provided
|
||||
const expected: {
|
||||
submissions: Schema.OrderSubmission[];
|
||||
stopOrdersSubmission: StopOrdersSubmission[];
|
||||
} = {
|
||||
stopOrdersSubmission: [
|
||||
{
|
||||
fallsBelow: {
|
||||
orderSubmission: {
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: undefined,
|
||||
reduceOnly: true,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
},
|
||||
price: '600000',
|
||||
},
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
expiresAt: undefined,
|
||||
marketId: 'marketId',
|
||||
postOnly: false,
|
||||
price: '663000',
|
||||
reduceOnly: false,
|
||||
reference: 'reference',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
size: '10000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
type: OrderType.TYPE_LIMIT,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"Notional": "Notional",
|
||||
"NOTIONAL_SIZE_TOOLTIP_TEXT": "The notional size represents the position size in the settlement asset {{quoteName}} of the futures contract. This is calculated by multiplying the number of contracts by the prices of the contract. For example 10 contracts traded at a price of $50 has a notional size of $500.",
|
||||
"OCO": "OCO",
|
||||
"One cancels the other": "One cancels the other",
|
||||
"One cancels another": "One cancels another",
|
||||
"Only limit orders are permitted when market is in auction": "Only limit orders are permitted when market is in auction",
|
||||
"Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.": "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.",
|
||||
"You have an existing position on this market.": "You have an existing position on this market.",
|
||||
@@ -135,12 +135,6 @@
|
||||
"Total margin available": "Total margin available",
|
||||
"TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).",
|
||||
"No trading": "No trading",
|
||||
"TP / SL": "TP / SL",
|
||||
"TP_SL_TOOLTIP": "Take profit / Stop loss",
|
||||
"Take profit": "Take profit",
|
||||
"Stop loss": "Stop loss",
|
||||
"The price for take profit.": "The price for take profit.",
|
||||
"The price for stop loss.": "The price for stop loss.",
|
||||
"Trailing percent offset cannot be higher than 99.9": "Trailing percent offset cannot be higher than 99.9",
|
||||
"Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}": "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}",
|
||||
"Trailing percentage offset": "Trailing percentage offset",
|
||||
|
||||
@@ -143,17 +143,12 @@
|
||||
"Hoarder reward multiplier": "Hoarder reward multiplier",
|
||||
"How it works": "How it works",
|
||||
"I want a code": "I want a code",
|
||||
"INTERVAL_I12H": "12H",
|
||||
"INTERVAL_I15M": "15m",
|
||||
"INTERVAL_I1D": "D",
|
||||
"INTERVAL_I1H": "1h",
|
||||
"INTERVAL_I1D": "1D",
|
||||
"INTERVAL_I1H": "1H",
|
||||
"INTERVAL_I1M": "1m",
|
||||
"INTERVAL_I30M": "30m",
|
||||
"INTERVAL_I4H": "4H",
|
||||
"INTERVAL_I5M": "5m",
|
||||
"INTERVAL_I6H": "6h",
|
||||
"INTERVAL_I8H": "8h",
|
||||
"INTERVAL_I7D": "W",
|
||||
"INTERVAL_I6H": "6H",
|
||||
"Improve vega console": "Improve vega console",
|
||||
"Inactive": "Inactive",
|
||||
"Index Price": "Index Price",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"Get MetaMask": "Get MetaMask",
|
||||
"Get the Vega Wallet": "Get the Vega Wallet",
|
||||
"I agree": "I agree",
|
||||
"Once you have the added the extension, <0>refresh</0> you browser.": "Once you have the added the extension, <0>refresh</0> you browser.",
|
||||
"Successfully connected": "Successfully connected",
|
||||
"Transaction was not successful": "Transaction was not successful",
|
||||
"Wallet rejected transaction": "Wallet rejected transaction"
|
||||
|
||||
@@ -15,14 +15,9 @@ export const TRADINGVIEW_INTERVAL_MAP = {
|
||||
[Interval.INTERVAL_I1M]: '1',
|
||||
[Interval.INTERVAL_I5M]: '5',
|
||||
[Interval.INTERVAL_I15M]: '15',
|
||||
[Interval.INTERVAL_I30M]: '30',
|
||||
[Interval.INTERVAL_I1H]: '60',
|
||||
[Interval.INTERVAL_I4H]: '240',
|
||||
[Interval.INTERVAL_I6H]: '360',
|
||||
[Interval.INTERVAL_I8H]: '480',
|
||||
[Interval.INTERVAL_I12H]: '720',
|
||||
[Interval.INTERVAL_I1D]: '1D',
|
||||
[Interval.INTERVAL_I7D]: '1W',
|
||||
} as const;
|
||||
|
||||
export type ResolutionRecord = typeof TRADINGVIEW_INTERVAL_MAP;
|
||||
|
||||
@@ -32,14 +32,9 @@ const resolutionMap: Record<string, Interval> = {
|
||||
'1': Interval.INTERVAL_I1M,
|
||||
'5': Interval.INTERVAL_I5M,
|
||||
'15': Interval.INTERVAL_I15M,
|
||||
'30': Interval.INTERVAL_I30M,
|
||||
'60': Interval.INTERVAL_I1H,
|
||||
'240': Interval.INTERVAL_I4H,
|
||||
'360': Interval.INTERVAL_I6H,
|
||||
'480': Interval.INTERVAL_I8H,
|
||||
'720': Interval.INTERVAL_I12H,
|
||||
'1D': Interval.INTERVAL_I1D,
|
||||
'1W': Interval.INTERVAL_I7D,
|
||||
} as const;
|
||||
|
||||
const supportedResolutions = Object.keys(resolutionMap);
|
||||
|
||||
@@ -17,8 +17,8 @@ export const InputError = ({
|
||||
...props
|
||||
}: InputErrorProps) => {
|
||||
const effectiveClassName = classNames(
|
||||
'text-sm flex items-center first-letter:uppercase',
|
||||
'mt-2',
|
||||
'text-sm block items-center first-letter:capitalize',
|
||||
'mt-2 min-w-0 break-words',
|
||||
{
|
||||
'border-danger': intent === 'danger',
|
||||
'border-warning': intent === 'warning',
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { type ReactNode, type FunctionComponent, forwardRef } from 'react';
|
||||
import {
|
||||
type ReactNode,
|
||||
type FunctionComponent,
|
||||
forwardRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
ConnectorErrors,
|
||||
isBrowserWalletInstalled,
|
||||
@@ -12,6 +17,7 @@ import { useConnect } from '../../hooks/use-connect';
|
||||
import { Links } from '../../constants';
|
||||
import { ConnectorIcon } from './connector-icon';
|
||||
import { useUserAgent } from '@vegaprotocol/react-helpers';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const vegaExtensionsLinks = {
|
||||
chrome: Links.chromeExtension,
|
||||
@@ -29,48 +35,67 @@ export const ConnectionOptions = ({
|
||||
onConnect: (id: ConnectorType) => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const error = useWallet((store) => store.error);
|
||||
const { connectors } = useConnect();
|
||||
const error = useWallet((store) => store.error);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<h2 className="text-xl">{t('Connect to Vega')}</h2>
|
||||
<ul
|
||||
className="grid grid-cols-1 sm:grid-cols-2 gap-1 -mx-2"
|
||||
data-testid="connectors-list"
|
||||
>
|
||||
{connectors.map((c) => {
|
||||
const ConnectionOption = ConnectionOptionRecord[c.id];
|
||||
const props = {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
showDescription: false,
|
||||
onClick: () => onConnect(c.id),
|
||||
};
|
||||
|
||||
if (ConnectionOption) {
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOption {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOptionDefault {...props} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{error && error.code !== ConnectorErrors.userRejected.code && (
|
||||
<p
|
||||
className="text-danger text-sm first-letter:uppercase"
|
||||
data-testid="connection-error"
|
||||
>
|
||||
{error.message}
|
||||
{isInstalling ? (
|
||||
<p className="text-warning">
|
||||
<Trans
|
||||
i18nKey="Once you have the added the extension, <0>refresh</0> you browser."
|
||||
components={[
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="underline underline-offset-4"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<ul
|
||||
className="grid grid-cols-1 sm:grid-cols-2 gap-1 -mx-2"
|
||||
data-testid="connectors-list"
|
||||
>
|
||||
{connectors.map((c) => {
|
||||
const ConnectionOption = ConnectionOptionRecord[c.id];
|
||||
const props = {
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
description: c.description,
|
||||
showDescription: false,
|
||||
onClick: () => onConnect(c.id),
|
||||
onInstall: () => setIsInstalling(true),
|
||||
};
|
||||
|
||||
if (ConnectionOption) {
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOption {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={c.id}>
|
||||
<ConnectionOptionDefault {...props} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{error && error.code !== ConnectorErrors.userRejected.code && (
|
||||
<p
|
||||
className="text-danger text-sm first-letter:uppercase"
|
||||
data-testid="connection-error"
|
||||
>
|
||||
{error.message}
|
||||
{error.data ? `: ${error.data}` : ''}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<a
|
||||
href={Links.walletOverview}
|
||||
@@ -90,6 +115,7 @@ interface ConnectionOptionProps {
|
||||
description: string;
|
||||
showDescription?: boolean;
|
||||
onClick: () => void;
|
||||
onInstall?: () => void;
|
||||
}
|
||||
|
||||
const CONNECTION_OPTION_CLASSES =
|
||||
@@ -142,6 +168,7 @@ export const ConnectionOptionInjected = ({
|
||||
description,
|
||||
showDescription = false,
|
||||
onClick,
|
||||
onInstall,
|
||||
}: ConnectionOptionProps) => {
|
||||
const t = useT();
|
||||
const userAgent = useUserAgent();
|
||||
@@ -158,7 +185,11 @@ export const ConnectionOptionInjected = ({
|
||||
</span>
|
||||
</ConnectionOptionButtonWithDescription>
|
||||
) : (
|
||||
<ConnectionOptionLinkWithDescription id={id} href={link}>
|
||||
<ConnectionOptionLinkWithDescription
|
||||
id={id}
|
||||
href={link}
|
||||
onClick={onInstall}
|
||||
>
|
||||
<span className="flex flex-col justify-start text-left">
|
||||
<span className="capitalize leading-5">
|
||||
{t('Get the Vega Wallet')}
|
||||
@@ -183,7 +214,7 @@ export const ConnectionOptionInjected = ({
|
||||
{name}
|
||||
</ConnectionOptionButton>
|
||||
) : (
|
||||
<ConnectionOptionLink id={id} href={link}>
|
||||
<ConnectionOptionLink id={id} href={link} onClick={onInstall}>
|
||||
{t('Get the Vega Wallet')}
|
||||
</ConnectionOptionLink>
|
||||
)}
|
||||
@@ -275,8 +306,9 @@ const ConnectionOptionLink = forwardRef<
|
||||
children: ReactNode;
|
||||
id: ConnectorType;
|
||||
href: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
>(({ children, id, href }, ref) => {
|
||||
>(({ children, id, href, onClick }, ref) => {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
@@ -285,6 +317,7 @@ const ConnectionOptionLink = forwardRef<
|
||||
className={CONNECTION_OPTION_CLASSES}
|
||||
data-testid={`connector-${id}`}
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ConnectorIcon id={id} />
|
||||
{children}
|
||||
@@ -320,8 +353,10 @@ const ConnectionOptionLinkWithDescription = forwardRef<
|
||||
children: ReactNode;
|
||||
id: ConnectorType;
|
||||
href: string;
|
||||
|
||||
onClick?: () => void;
|
||||
}
|
||||
>(({ children, id, href }, ref) => {
|
||||
>(({ children, id, href, onClick }, ref) => {
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
@@ -329,6 +364,7 @@ const ConnectionOptionLinkWithDescription = forwardRef<
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span>
|
||||
<ConnectorIcon id={id} />
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useWallet } from './use-wallet';
|
||||
import { useConnect } from './use-connect';
|
||||
|
||||
export function useEagerConnect() {
|
||||
const current = useWallet((store) => store.current);
|
||||
const status = useWallet((store) => store.status);
|
||||
const { connect } = useConnect();
|
||||
const [connecting, setConnecting] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const attemptConnect = async () => {
|
||||
// No stored config, or config was malformed or no risk accepted
|
||||
if (!current) {
|
||||
setConnecting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status !== 'disconnected') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,15 +22,13 @@ export function useEagerConnect() {
|
||||
await connect(current);
|
||||
} catch {
|
||||
console.warn(`Failed to connect with connector: ${current}`);
|
||||
} finally {
|
||||
setConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
attemptConnect();
|
||||
}
|
||||
}, [connect, current, connecting]);
|
||||
}, [status, connect, current]);
|
||||
|
||||
return connecting;
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ export const useSimpleTransaction = (opts?: Options) => {
|
||||
if (err.code === ConnectorErrors.userRejected.code) {
|
||||
setStatus('idle');
|
||||
} else {
|
||||
setError(err.message);
|
||||
setError(`${err.message}${err.data ? `: ${err.data}` : ''}`);
|
||||
setStatus('idle');
|
||||
opts?.onError?.(err.message);
|
||||
}
|
||||
} else {
|
||||
const msg = t('Wallet rejected transaction');
|
||||
const msg = t('Something went wrong');
|
||||
setError(msg);
|
||||
setStatus('idle');
|
||||
opts?.onError?.(msg);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
listKeysError,
|
||||
noWalletError,
|
||||
sendTransactionError,
|
||||
userRejectedError,
|
||||
} from '../errors';
|
||||
import {
|
||||
type TransactionParams,
|
||||
@@ -14,6 +15,19 @@ import {
|
||||
type VegaWalletEvent,
|
||||
} from '../types';
|
||||
|
||||
interface InjectedError {
|
||||
message: string;
|
||||
code: number;
|
||||
data:
|
||||
| {
|
||||
message: string;
|
||||
code: number;
|
||||
}
|
||||
| string;
|
||||
}
|
||||
|
||||
const USER_REJECTED_CODE = -4;
|
||||
|
||||
export class InjectedConnector implements Connector {
|
||||
readonly id = 'injected';
|
||||
readonly name = 'Vega Wallet';
|
||||
@@ -85,15 +99,55 @@ export class InjectedConnector implements Connector {
|
||||
sentAt: res.sentAt,
|
||||
};
|
||||
} catch (err) {
|
||||
if (this.isInjectedError(err)) {
|
||||
if (err.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
|
||||
if (typeof err.data === 'string') {
|
||||
throw sendTransactionError(err.data);
|
||||
} else {
|
||||
throw sendTransactionError(err.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw sendTransactionError();
|
||||
}
|
||||
}
|
||||
|
||||
on(event: VegaWalletEvent, callback: () => void) {
|
||||
window.vega.on(event, callback);
|
||||
// Check for on/off in case user is on older versions which don't support it
|
||||
// We can remove this check once FF is at the latest version
|
||||
if (
|
||||
typeof window.vega !== 'undefined' &&
|
||||
typeof window.vega.on === 'function'
|
||||
) {
|
||||
window.vega.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
off(event: VegaWalletEvent, callback: () => void) {
|
||||
window.vega.off(event, callback);
|
||||
// Check for on/off in case user is on older versions which don't support it
|
||||
// We can remove this check once FF is at the latest version
|
||||
if (
|
||||
typeof window.vega !== 'undefined' &&
|
||||
typeof window.vega.off === 'function'
|
||||
) {
|
||||
window.vega.off(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private isInjectedError(obj: unknown): obj is InjectedError {
|
||||
if (
|
||||
obj !== undefined &&
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'code' in obj &&
|
||||
'message' in obj &&
|
||||
'data' in obj
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
|
||||
type JsonRpcConnectorConfig = { url: string; token?: string };
|
||||
|
||||
const USER_REJECTED_CODE = 3001;
|
||||
|
||||
export class JsonRpcConnector implements Connector {
|
||||
readonly id = 'jsonRpc';
|
||||
readonly name = 'Command Line Wallet';
|
||||
@@ -27,7 +29,7 @@ export class JsonRpcConnector implements Connector {
|
||||
requestId: number = 0;
|
||||
store: StoreApi<Store> | undefined;
|
||||
pollRef: NodeJS.Timer | undefined;
|
||||
ee: EventEmitter;
|
||||
ee: InstanceType<typeof EventEmitter>;
|
||||
|
||||
constructor(config: JsonRpcConnectorConfig) {
|
||||
this.url = config.url;
|
||||
@@ -63,7 +65,7 @@ export class JsonRpcConnector implements Connector {
|
||||
const token = response.headers.get('Authorization');
|
||||
|
||||
if (!response.ok) {
|
||||
if ('error' in data && data.error.code === 3001) {
|
||||
if ('error' in data && data.error.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
throw connectError('response not ok');
|
||||
@@ -137,7 +139,7 @@ export class JsonRpcConnector implements Connector {
|
||||
|
||||
if (!response.ok) {
|
||||
if ('error' in data) {
|
||||
if (data.error.code === 3001) {
|
||||
if (data.error.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
ConnectorError,
|
||||
chainIdError,
|
||||
@@ -6,13 +5,13 @@ import {
|
||||
listKeysError,
|
||||
noWalletError,
|
||||
sendTransactionError,
|
||||
userRejectedError,
|
||||
} from '../errors';
|
||||
import { type Transaction } from '../transaction-types';
|
||||
import {
|
||||
JsonRpcMethod,
|
||||
type Connector,
|
||||
type TransactionParams,
|
||||
type VegaWalletEvent,
|
||||
} from '../types';
|
||||
|
||||
enum EthereumMethod {
|
||||
@@ -43,7 +42,6 @@ declare global {
|
||||
type WindowEthereumProvider = {
|
||||
isMetaMask: boolean;
|
||||
request<T = unknown>(args: RequestArguments): Promise<T>;
|
||||
selectedAddress: string | null;
|
||||
};
|
||||
|
||||
interface Window {
|
||||
@@ -52,6 +50,16 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface SnapRPCError {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: {
|
||||
originalError: { code: number };
|
||||
};
|
||||
}
|
||||
|
||||
const USER_REJECTED_CODE = -4;
|
||||
|
||||
export class SnapConnector implements Connector {
|
||||
readonly id = 'snap';
|
||||
readonly name = 'MetaMask Snap';
|
||||
@@ -61,8 +69,6 @@ export class SnapConnector implements Connector {
|
||||
node: string;
|
||||
version: string;
|
||||
snapId: string;
|
||||
pollRef: NodeJS.Timer | undefined;
|
||||
ee: EventEmitter;
|
||||
|
||||
// Note: apps may not know which node is selected on start up so its up
|
||||
// to the app to make sure class intances are renewed if the node changes
|
||||
@@ -70,14 +76,21 @@ export class SnapConnector implements Connector {
|
||||
this.node = config.node;
|
||||
this.version = config.version;
|
||||
this.snapId = config.snapId;
|
||||
this.ee = new EventEmitter();
|
||||
}
|
||||
|
||||
bindStore() {}
|
||||
|
||||
async connectWallet(desiredChainId: string) {
|
||||
try {
|
||||
await this.requestSnap();
|
||||
const res = await this.requestSnap();
|
||||
|
||||
if (res[this.snapId].blocked) {
|
||||
throw connectError('snap is blocked');
|
||||
}
|
||||
|
||||
if (!res[this.snapId].enabled) {
|
||||
throw connectError('snap is not enabled');
|
||||
}
|
||||
|
||||
const { chainId } = await this.getChainId();
|
||||
|
||||
@@ -87,7 +100,6 @@ export class SnapConnector implements Connector {
|
||||
);
|
||||
}
|
||||
|
||||
this.startPoll();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (err instanceof ConnectorError) {
|
||||
@@ -98,57 +110,66 @@ export class SnapConnector implements Connector {
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectWallet() {
|
||||
this.stopPoll();
|
||||
}
|
||||
async disconnectWallet() {}
|
||||
|
||||
// deprecated, pass chain on connect
|
||||
async getChainId() {
|
||||
try {
|
||||
const res = await this.invokeSnap<{ chainID: string }>(
|
||||
const data = await this.invokeSnap<{ chainID: string }>(
|
||||
JsonRpcMethod.GetChainId,
|
||||
{
|
||||
networkEndpoints: [this.node],
|
||||
}
|
||||
);
|
||||
return { chainId: res.chainID };
|
||||
|
||||
if ('error' in data) {
|
||||
throw chainIdError(data.error.message);
|
||||
}
|
||||
|
||||
return { chainId: data.chainID };
|
||||
} catch (err) {
|
||||
this.stopPoll();
|
||||
if (err instanceof ConnectorError) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw chainIdError();
|
||||
}
|
||||
}
|
||||
|
||||
async listKeys() {
|
||||
try {
|
||||
const res = await this.invokeSnap<{
|
||||
const data = await this.invokeSnap<{
|
||||
keys: Array<{ publicKey: string; name: string }>;
|
||||
}>(JsonRpcMethod.ListKeys);
|
||||
return res.keys;
|
||||
|
||||
if ('error' in data) {
|
||||
throw listKeysError(data.error.message);
|
||||
}
|
||||
|
||||
return data.keys;
|
||||
} catch (err) {
|
||||
this.stopPoll();
|
||||
if (err instanceof ConnectorError) {
|
||||
throw err;
|
||||
}
|
||||
throw listKeysError();
|
||||
}
|
||||
}
|
||||
|
||||
async isConnected() {
|
||||
try {
|
||||
// Check if metamask is unlocked
|
||||
if (!window.ethereum.selectedAddress) {
|
||||
throw noWalletError();
|
||||
}
|
||||
|
||||
// If this throws its likely the snap is disabled or has been uninstalled
|
||||
await this.listKeys();
|
||||
return { connected: true };
|
||||
} catch (err) {
|
||||
this.stopPoll();
|
||||
return { connected: false };
|
||||
}
|
||||
}
|
||||
|
||||
async sendTransaction(params: TransactionParams) {
|
||||
try {
|
||||
const res = await this.invokeSnap<{
|
||||
// If the transaction is invalid this will throw with SnapRPCError
|
||||
// but if its rejected it will resolve with 'error' in data
|
||||
const data = await this.invokeSnap<{
|
||||
transactionHash: string;
|
||||
transaction: { signature: { value: string } };
|
||||
receivedAt: string;
|
||||
@@ -160,115 +181,99 @@ export class SnapConnector implements Connector {
|
||||
networkEndpoints: [this.node],
|
||||
});
|
||||
|
||||
if ('error' in data) {
|
||||
if (data.error.code === USER_REJECTED_CODE) {
|
||||
throw userRejectedError();
|
||||
}
|
||||
|
||||
throw sendTransactionError(`${data.error.message}: ${data.error.data}`);
|
||||
}
|
||||
|
||||
return {
|
||||
transactionHash: res.transactionHash,
|
||||
signature: res.transaction.signature.value,
|
||||
receivedAt: res.receivedAt,
|
||||
sentAt: res.sentAt,
|
||||
transactionHash: data.transactionHash,
|
||||
signature: data.transaction.signature.value,
|
||||
receivedAt: data.receivedAt,
|
||||
sentAt: data.sentAt,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof ConnectorError) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (this.isSnapRPCError(err)) {
|
||||
throw sendTransactionError(err.message);
|
||||
}
|
||||
|
||||
throw sendTransactionError();
|
||||
}
|
||||
}
|
||||
|
||||
on(event: VegaWalletEvent, callback: () => void) {
|
||||
this.ee.on(event, callback);
|
||||
}
|
||||
on() {}
|
||||
|
||||
off() {}
|
||||
|
||||
off(event: VegaWalletEvent, callback?: () => void) {
|
||||
this.ee.off(event, callback);
|
||||
}
|
||||
////////////////////////////////////
|
||||
// Snap methods
|
||||
////////////////////////////////////
|
||||
|
||||
private startPoll() {
|
||||
// This only event we need to poll for right now is client.disconnect,
|
||||
// if more events get added we will need more logic here
|
||||
this.pollRef = setInterval(async () => {
|
||||
const result = await this.isConnected();
|
||||
if (result.connected) return;
|
||||
this.ee.emit('client.disconnected');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private stopPoll() {
|
||||
if (this.pollRef) {
|
||||
clearInterval(this.pollRef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permission for a website to communicate with the specified snaps
|
||||
* and attempts to install them if they're not already installed.
|
||||
* If the installation of any snap fails, returns the error that caused the failure.
|
||||
* More informations here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_requestsnaps
|
||||
*/
|
||||
private async requestSnap() {
|
||||
await this.request(EthereumMethod.RequestSnaps, {
|
||||
[this.snapId]: {
|
||||
version: this.version,
|
||||
private async requestSnap(): Promise<{
|
||||
[snapId: string]: {
|
||||
blocked: boolean;
|
||||
enabled: boolean;
|
||||
id: string;
|
||||
version: string;
|
||||
};
|
||||
}> {
|
||||
return window.ethereum.request({
|
||||
method: EthereumMethod.RequestSnaps,
|
||||
params: {
|
||||
[this.snapId]: {
|
||||
version: this.version,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: check if this is needed, its used in use-snap-status
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * Gets the list of all installed snaps.
|
||||
// * More information here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_getsnaps
|
||||
// */
|
||||
// async getSnap() {
|
||||
// const snaps = await this.request(EthereumMethod.GetSnaps);
|
||||
// return Object.values(snaps).find(
|
||||
// (s) => s.id === this.snapId && s.version === this.version
|
||||
// );
|
||||
// }
|
||||
|
||||
/**
|
||||
* Calls a method on the specified snap, always vega in this case
|
||||
* should always be npm:@vegaprotocol/snap
|
||||
*/
|
||||
private async invokeSnap<TResult>(
|
||||
method: JsonRpcMethod,
|
||||
params?: SnapInvocationParams
|
||||
): Promise<TResult> {
|
||||
return await this.request(EthereumMethod.InvokeSnap, {
|
||||
snapId: this.snapId,
|
||||
request: {
|
||||
method,
|
||||
params,
|
||||
params: SnapInvocationParams = {}
|
||||
): Promise<TResult | { error: SnapRPCError }> {
|
||||
// MetaMask in Firefox doesn't like undefined properties or some properties
|
||||
// on __proto__ so we need to strip them out with JSON.strinfify
|
||||
params = JSON.parse(JSON.stringify(params));
|
||||
|
||||
return window.ethereum.request({
|
||||
method: EthereumMethod.InvokeSnap,
|
||||
params: {
|
||||
snapId: this.snapId,
|
||||
request: {
|
||||
method,
|
||||
params,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls window.ethereum.request with method and params
|
||||
*/
|
||||
private async request<TResult>(
|
||||
method: EthereumMethod,
|
||||
params?: object
|
||||
): Promise<TResult> {
|
||||
if (window.ethereum?.request && window.ethereum?.isMetaMask) {
|
||||
// MetaMask in Firefox doesn't like undefined properties or some properties
|
||||
// on __proto__ so we need to strip them out with JSON.strinfify
|
||||
try {
|
||||
params = JSON.parse(JSON.stringify(params));
|
||||
} catch (err) {
|
||||
throw sendTransactionError();
|
||||
}
|
||||
|
||||
return window.ethereum.request({
|
||||
method,
|
||||
params,
|
||||
});
|
||||
private isSnapRPCError(obj: unknown): obj is SnapRPCError {
|
||||
if (
|
||||
obj !== undefined &&
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'code' in obj &&
|
||||
'message' in obj
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw noWalletError();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class ConnectorError extends Error {
|
||||
|
||||
export const ConnectorErrors = {
|
||||
userRejected: { message: 'user rejected', code: 0 },
|
||||
noConnector: { message: 'no connector', code: 1 },
|
||||
noConnector: { message: 'not connected', code: 1 },
|
||||
connect: { message: 'failed to connect', code: 2 },
|
||||
disconnect: { message: 'failed to disconnect', code: 3 },
|
||||
chainId: { message: 'incorrect chain id', code: 4 },
|
||||
|
||||
@@ -403,9 +403,6 @@ export interface BatchMarketInstructionSubmissionBody {
|
||||
// Note: If multiple orders are submitted the first order ID is determined by hashing the signature of the transaction
|
||||
// (see determineId function). For each subsequent order's ID, a hash of the previous orders ID is used
|
||||
submissions?: OrderSubmission[];
|
||||
stopOrdersSubmission?: StopOrdersSubmission[];
|
||||
stopOrdersCancellation?: StopOrdersCancellation[];
|
||||
updateMarginMode?: UpdateMarginMode[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,6 @@ describe('disconnect', () => {
|
||||
expect(result).toEqual({ status: 'disconnected' });
|
||||
expect(config.store.getState()).toMatchObject({
|
||||
status: 'disconnected',
|
||||
error: noConnectorError(),
|
||||
current: undefined,
|
||||
keys: [],
|
||||
pubKey: undefined,
|
||||
@@ -130,7 +129,7 @@ describe('refresh keys', () => {
|
||||
it('handles invalid connector', async () => {
|
||||
await config.refreshKeys();
|
||||
expect(config.store.getState()).toMatchObject({
|
||||
error: noConnectorError(),
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -132,18 +132,20 @@ export function createConfig(cfg: Config): Wallet {
|
||||
store.setState(getInitialState(), true);
|
||||
return { status: 'disconnected' as const };
|
||||
} catch (err) {
|
||||
store.setState({
|
||||
...getInitialState(),
|
||||
error: err instanceof ConnectorError ? err : unknownError(),
|
||||
});
|
||||
store.setState(getInitialState(), true);
|
||||
return { status: 'disconnected' as const };
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshKeys() {
|
||||
const connector = connectors
|
||||
.getState()
|
||||
.find((x) => x.id === store.getState().current);
|
||||
const state = store.getState();
|
||||
const connector = connectors.getState().find((x) => x.id === state.current);
|
||||
|
||||
// Only refresh keys if connnected. If you aren't connect when you connect
|
||||
// you will get the latest keys
|
||||
if (state.status !== 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!connector) {
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@
|
||||
"jsondiffpatch": "^0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "13.3.0",
|
||||
"pennant": "^1.16.2",
|
||||
"pennant": "^1.15.0",
|
||||
"react": "18.2.0",
|
||||
"react-copy-to-clipboard": "5.1.0",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@@ -1460,20 +1460,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
||||
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.20.7", "@babel/runtime@^7.22.6", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.6", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.23.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
|
||||
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.13.10", "@babel/runtime@^7.21.0":
|
||||
version "7.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e"
|
||||
integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.22.5":
|
||||
version "7.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e"
|
||||
@@ -2445,22 +2438,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4"
|
||||
integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==
|
||||
|
||||
"@floating-ui/core@^1.0.0", "@floating-ui/core@^1.4.2":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
|
||||
integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
|
||||
"@floating-ui/core@^1.4.2":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c"
|
||||
integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.1"
|
||||
"@floating-ui/utils" "^0.1.3"
|
||||
|
||||
"@floating-ui/dom@^1.2.1":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
|
||||
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.0.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
|
||||
"@floating-ui/dom@^1.5.1":
|
||||
"@floating-ui/dom@^1.2.1", "@floating-ui/dom@^1.5.1":
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
|
||||
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
|
||||
@@ -2487,11 +2472,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
|
||||
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
|
||||
|
||||
"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||
|
||||
"@graphql-codegen/add@^3.2.1":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-3.2.3.tgz#f1ecee085987e7c21841edc4b1fd48877c663e1a"
|
||||
@@ -7044,9 +7024,9 @@
|
||||
integrity sha512-5a21DF7avVPmiUau8KTsv5r76yGqbMgq4QtByoCBPXUrVFWFkd3Ob4OOhmePNRbQqfUCNFjgB4sO7sUURnKcBg==
|
||||
|
||||
"@types/d3-shape@^2.0.0":
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.7.tgz#7c3bd6a9c758b54ba495cab0575cb18359251123"
|
||||
integrity sha512-HedHlfGHdwzKqX9+PiQVXZrdmGlwo7naoefJP7kCNk4Y7qcpQt1tUaoRa6qn0kbTdlaIHGO7111qLtb/6J8uuw==
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.6.tgz#38b161512d303c69e709df573db203f199343324"
|
||||
integrity sha512-UvUXi3uJk7i9gstNlyh/+lidKy96AVp6lG6it586lYVIHjS2oRKkOSfaWdON6+Ziu+EqB8kbN3onxk+eP2wSmw==
|
||||
dependencies:
|
||||
"@types/d3-path" "^2"
|
||||
|
||||
@@ -7320,16 +7300,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash@^4.14.167", "@types/lodash@^4.14.171":
|
||||
"@types/lodash@^4.14.167", "@types/lodash@^4.14.168", "@types/lodash@^4.14.171":
|
||||
version "4.14.201"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.201.tgz#76f47cb63124e806824b6c18463daf3e1d480239"
|
||||
integrity sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==
|
||||
|
||||
"@types/lodash@^4.14.168":
|
||||
version "4.14.202"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8"
|
||||
integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==
|
||||
|
||||
"@types/mdast@^3.0.0":
|
||||
version "3.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5"
|
||||
@@ -7416,14 +7391,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.61.tgz#5ea47e3018348bf3bbbe646b396ba5e720310be1"
|
||||
integrity sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q==
|
||||
|
||||
"@types/node@^18.0.0":
|
||||
version "18.19.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.21.tgz#f4ca1ac8ffb05ee4b89163c2d6fac9a1a59ee149"
|
||||
integrity sha512-2Q2NeB6BmiTFQi4DHBzncSoq/cJMLDdhPaAoJFnFCyD9a8VPZRf7a1GAwp1Edb7ROaZc5Jz/tnZyL6EsWMRaqw==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@^18.17.5":
|
||||
"@types/node@^18.0.0", "@types/node@^18.17.5":
|
||||
version "18.18.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.9.tgz#5527ea1832db3bba8eb8023ce8497b7d3f299592"
|
||||
integrity sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==
|
||||
@@ -7457,12 +7425,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.3.tgz#47fe8e784c2dee24fe636cab82e090d3da9b7dec"
|
||||
integrity sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
|
||||
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
|
||||
|
||||
"@types/prop-types@^15.0.0":
|
||||
"@types/prop-types@*", "@types/prop-types@^15.0.0":
|
||||
version "15.7.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.10.tgz#892afc9332c4d62a5ea7e897fe48ed2085bbb08a"
|
||||
integrity sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==
|
||||
@@ -7491,20 +7454,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@^18.0.0":
|
||||
"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.5":
|
||||
version "18.2.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.15.tgz#921af67f9ee023ac37ea84b1bc0cc40b898ea522"
|
||||
integrity sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@^18.0.5":
|
||||
version "18.2.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.20.tgz#cbdf7abb3cc2377980bb1294bc51375016a8320f"
|
||||
integrity sha512-HXN/biJY8nv20Cn9ZbCFq3liERd4CozVZmKbaiZ9KiKTrWqsP7eoGDO6OOGvJQwoVFuiXaiJ7nBBjiFFbRmQMQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-dom@^5.3.3":
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
|
||||
@@ -7529,14 +7485,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-virtualized-auto-sizer@^1.0.0":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.4.tgz#42044ef75ac2d2667893a5943e54a9f037f985a3"
|
||||
integrity sha512-nhYwlFiYa8M3S+O2T9QO/e1FQUYMr/wJENUdf/O0dhRi1RS/93rjrYQFYdbUqtdFySuhrtnEDX29P6eKOttY+A==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-virtualized-auto-sizer@^1.0.1":
|
||||
"@types/react-virtualized-auto-sizer@^1.0.0", "@types/react-virtualized-auto-sizer@^1.0.1":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.3.tgz#13f4387c1b0b635b89d403970863b1ff464cd91e"
|
||||
integrity sha512-xRsQJiM8BuwGiDl77yyFZqq32lLvI4msFtw7nVbw9qh9c2LvchDXezwjEWmysJkXnLZWjHJX9lT8MCPkFy5BfQ==
|
||||
@@ -7558,10 +7507,10 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.0.14":
|
||||
version "18.2.63"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.63.tgz#4637c56146ad90f96d0583171edab953f7e6fe57"
|
||||
integrity sha512-ppaqODhs15PYL2nGUOaOu2RSCCB4Difu4UFrP4I3NHLloXC/ESQzQMi9nvjfT1+rudd0d2L3fQPJxRSey+rGlQ==
|
||||
"@types/react@*", "@types/react@>=16", "@types/react@^18.0.14":
|
||||
version "18.2.37"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.37.tgz#0f03af69e463c0f19a356c2660dbca5d19c44cae"
|
||||
integrity sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
@@ -7576,15 +7525,6 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@>=16":
|
||||
version "18.2.37"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.37.tgz#0f03af69e463c0f19a356c2660dbca5d19c44cae"
|
||||
integrity sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
|
||||
@@ -7605,9 +7545,9 @@
|
||||
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
|
||||
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
|
||||
version "0.16.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.6.tgz#eb26db6780c513de59bee0b869ef289ad3068711"
|
||||
integrity sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==
|
||||
|
||||
"@types/semver@^7.3.12", "@types/semver@^7.3.4", "@types/semver@^7.5.0":
|
||||
version "7.5.5"
|
||||
@@ -10059,22 +9999,7 @@ check-more-types@^2.24.0:
|
||||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
|
||||
integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==
|
||||
|
||||
"chokidar@>=3.0.0 <4.0.0":
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
|
||||
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^3.5.2, chokidar@^3.5.3:
|
||||
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2, chokidar@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
||||
@@ -10126,16 +10051,11 @@ cjs-module-lexer@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107"
|
||||
integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==
|
||||
|
||||
classnames@*, classnames@^2.2, classnames@^2.2.5, classnames@^2.3.1:
|
||||
classnames@*, classnames@^2.2, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.0, classnames@^2.3.1:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||
|
||||
classnames@^2.2.6, classnames@^2.3.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||
|
||||
clean-css@^5.2.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224"
|
||||
@@ -10970,9 +10890,9 @@ cssstyle@^2.3.0:
|
||||
cssom "~0.3.6"
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
cypress-mochawesome-reporter@^3.3.0:
|
||||
version "3.6.1"
|
||||
@@ -11480,11 +11400,11 @@ del@^6.0.0:
|
||||
slash "^3.0.0"
|
||||
|
||||
delaunator@5:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278"
|
||||
integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
|
||||
integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
|
||||
dependencies:
|
||||
robust-predicates "^3.0.2"
|
||||
robust-predicates "^3.0.0"
|
||||
|
||||
delay@^5.0.0:
|
||||
version "5.0.0"
|
||||
@@ -14394,9 +14314,9 @@ immer@^9.0.12:
|
||||
integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0"
|
||||
integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
|
||||
integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
|
||||
|
||||
immutable@~3.7.6:
|
||||
version "3.7.6"
|
||||
@@ -17983,10 +17903,10 @@ pend@~1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||
|
||||
pennant@^1.16.2:
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.16.2.tgz#5c6a63a2beda07ff86f7e33400d8570c171ac479"
|
||||
integrity sha512-/n1GzSWZFlgYCfSZubmZA2eiIoOvZPJJP9RKc/u2pOIPlp5Bn2Lq5uKyAT9bvjh/YDQtMhBKf92Q6DxJEDnJNw==
|
||||
pennant@^1.15.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.15.0.tgz#21854cf78466cbd27eda8143c21abcde070d4d76"
|
||||
integrity sha512-p3H4vu6BP7nUqn7s2pjyTl0FMJUT2U5lq5UKuxy55F2E66sJnvp5XFvNjRZvi42vyzsxG8WURgwQDQKNqQgWAw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@d3fc/d3fc-technical-indicator" "^8.0.1"
|
||||
@@ -19299,12 +19219,7 @@ react-use-websocket@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-3.0.0.tgz#754cb8eea76f55d31c5676d4abe3e573bc2cea04"
|
||||
integrity sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==
|
||||
|
||||
react-virtualized-auto-sizer@^1.0.4:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.23.tgz#ddb18f775a00f672577f1ec01306a94ca26161b8"
|
||||
integrity sha512-5id3UTx+fG7b7SIOKL9/7aR1vP8+MtIT84cJCf09F6pYalB/nvHlx5EQvsSk27SwHUKjgPamG/nS8ynI0uSfKA==
|
||||
|
||||
react-virtualized-auto-sizer@^1.0.6:
|
||||
react-virtualized-auto-sizer@^1.0.4, react-virtualized-auto-sizer@^1.0.6:
|
||||
version "1.0.20"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz#d9a907253a7c221c52fa57dc775a6ef40c182645"
|
||||
integrity sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==
|
||||
@@ -19515,9 +19430,9 @@ regenerator-runtime@^0.13.7:
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
regenerator-runtime@^0.14.0:
|
||||
version "0.14.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
||||
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
|
||||
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
|
||||
|
||||
regenerator-transform@^0.15.2:
|
||||
version "0.15.2"
|
||||
@@ -19812,7 +19727,7 @@ rimraf@~2.6.2:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
robust-predicates@^3.0.2:
|
||||
robust-predicates@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
||||
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
|
||||
@@ -20001,7 +19916,7 @@ sass@1.55.0:
|
||||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
sass@^1.42.1:
|
||||
sass@^1.42.1, sass@^1.49.9:
|
||||
version "1.69.5"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde"
|
||||
integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==
|
||||
@@ -20010,15 +19925,6 @@ sass@^1.42.1:
|
||||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
sass@^1.49.9:
|
||||
version "1.71.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.71.1.tgz#dfb09c63ce63f89353777bbd4a88c0a38386ee54"
|
||||
integrity sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
sax@^1.2.4:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
|
||||
|
||||
Reference in New Issue
Block a user