Compare commits

..
59 changed files with 2782 additions and 1076 deletions
+1 -1
View File
@@ -290,5 +290,5 @@ jobs:
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
exit 0
fi
@@ -16,9 +16,7 @@ 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';
@@ -26,6 +24,12 @@ 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;
@@ -106,9 +110,11 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined,
});
const {
data: previewData,
loading: previewLoading,
isEligible: isPreviewEligible,
} = useReferralSet(validateCode(codeField, t) ? codeField : undefined);
const { send, status } = useSimpleTransaction({
onSuccess: () => {
@@ -141,19 +147,14 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
* Validates the set a user tries to apply to.
*/
const validateSet = useCallback(() => {
if (
codeField &&
!previewLoading &&
previewData &&
!previewData.isEligible
) {
if (codeField && !previewLoading && previewData && !isPreviewEligible) {
return t('The code is no longer valid.');
}
if (codeField && !previewLoading && !previewData) {
return t('The code is invalid');
}
return true;
}, [codeField, previewData, previewLoading, t]);
}, [codeField, isPreviewEligible, previewData, previewLoading, t]);
const noFunds = validateFundsAvailable() !== true ? true : false;
@@ -200,8 +201,6 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
// });
};
const { epochsValue, nextBenefitTierValue } = useStats({ program });
// show "code applied" message when successfully applied
if (status === 'confirmed') {
return (
@@ -264,9 +263,10 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
};
};
const nextBenefitTierEpochsValue = nextBenefitTierValue
? nextBenefitTierValue.epochs - epochsValue
: 0;
// 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;
return (
<>
@@ -335,17 +335,17 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
<Loader />
</div>
) : null}
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData && previewData.isEligible ? (
{previewData && isPreviewEligible ? (
<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: nextBenefitTierEpochsValue }
{ count: minEpochs }
)}
</h2>
<Statistics data={previewData} program={program} as="referee" />
<PreviewRefereeStatistics setId={codeField} />
</div>
) : null}
</>
@@ -1,3 +1,6 @@
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';
@@ -8,3 +11,19 @@ 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,13 +16,16 @@ 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();
@@ -145,7 +148,7 @@ const CreateCodeDialog = ({
const t = useT();
const createLink = useLinks(DApp.Governance);
const { pubKey } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' });
const { refetch } = useFindReferralSet(pubKey);
const {
err,
code,
@@ -0,0 +1,122 @@
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);
};
@@ -0,0 +1,117 @@
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,
};
};
@@ -0,0 +1,107 @@
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,8 +1,12 @@
import { formatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
import {
type ReferralProgramQuery,
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 = {
@@ -82,7 +86,37 @@ const MOCK = {
},
};
export const useReferralProgram = () => {
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 => {
const { data, loading, error } = useReferralProgramQuery({
fetchPolicy: 'cache-and-network',
});
@@ -5,20 +5,22 @@ 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, refetch } = useReferral({ pubKey, role: 'referee' });
const { data, loading, role, isEligible, refetch } =
useFindReferralSet(pubKey);
const {
data: epochData,
loading: epochLoading,
@@ -36,7 +38,13 @@ const useNonEligibleReferralSet = () => {
};
}, [epochRefetch, refetch]);
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
return {
data,
isEligible,
role,
epoch: epochData?.epoch.id,
loading: loading || epochLoading,
};
};
export const useReferralToasts = () => {
@@ -49,14 +57,16 @@ export const useReferralToasts = () => {
store.update,
]);
const { data, epoch, loading } = useNonEligibleReferralSet();
const { data, role, isEligible, epoch, loading } =
useNonEligibleReferralSet();
useEffect(() => {
if (
data &&
role === 'referee' &&
epoch &&
!loading &&
!data.isEligible &&
!isEligible &&
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
) {
const nonEligibleReferralToast: Toast = {
@@ -98,9 +108,11 @@ export const useReferralToasts = () => {
data,
epoch,
hasToast,
isEligible,
loading,
navigate,
pathname,
role,
setToast,
t,
updateToast,
@@ -1,217 +0,0 @@
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)
);
};
@@ -0,0 +1,104 @@
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,
};
};
@@ -0,0 +1,202 @@
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>
);
};
@@ -0,0 +1,146 @@
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,597 +1,58 @@
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 { Loader } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet-react';
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 { ApplyCodeFormContainer } from './apply-code-form';
import { useReferralProgram } from './hooks/use-referral-program';
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';
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';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
const program = useReferralProgram();
const { data: referee, refetch: refereeRefetch } = useReferral({
pubKey,
role: 'referee',
aggregationEpochs: program.details?.windowLength,
});
const {
data: referralSet,
loading: referralSetLoading,
role,
refetch,
} = useFindReferralSet(pubKey);
const { data: referrer, refetch: referrerRefetch } = useUpdateReferees(
useReferral({
pubKey,
role: 'referrer',
aggregationEpochs: program.details?.windowLength,
}),
DEFAULT_AGGREGATION_DAYS,
['totalRefereeGeneratedRewards'],
DEFAULT_AGGREGATION_DAYS === program.details?.windowLength
);
if (referralSetLoading) {
return <Loader size="small" />;
}
const refetch = useCallback(() => {
refereeRefetch();
referrerRefetch();
}, [refereeRefetch, referrerRefetch]);
const aggregationEpochs =
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
if (referee?.code) {
if (referralSet?.id && role === 'referrer') {
return (
<>
<Statistics data={referee} program={program} as="referee" />
{!referee.isEligible && <ApplyCodeForm />}
<ReferrerStatistics
aggregationEpochs={aggregationEpochs}
createdAt={referralSet.createdAt}
setId={referralSet.id}
/>
<Referees
setId={referralSet.id}
aggregationEpochs={aggregationEpochs}
/>
</>
);
}
if (referrer?.code) {
if (pubKey && referralSet?.id && role === 'referee') {
return (
<>
<Statistics data={referrer} program={program} as="referrer" />
<RefereesTable data={referrer} program={program} />
</>
<RefereeStatistics
aggregationEpochs={aggregationEpochs}
pubKey={pubKey}
referrerPubKey={referralSet.referrer}
setId={referralSet.id}
/>
);
}
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,26 +34,9 @@ export const Referrals = () => {
const t = useT();
const { pubKey } = useVegaWallet();
const {
data: referee,
loading: refereeLoading,
error: refereeError,
} = useReferral({
pubKey,
role: 'referee',
});
const {
data: referrer,
loading: referrerLoading,
error: referrerError,
} = useReferral({
pubKey,
role: 'referrer',
});
const { data, loading, error } = useFindReferralSet(pubKey);
const error = refereeError || referrerError;
const loading = refereeLoading || referrerLoading;
const showNav = !loading && !error && !referrer && !referee;
const showNav = !loading && !error && !data;
usePageTitle(t('Referrals'));
@@ -0,0 +1,73 @@
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>
);
};
@@ -0,0 +1,440 @@
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
)} &times; ${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,7 +6,12 @@ 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;
@@ -0,0 +1 @@
export { ProfileDialog } from './profile-dialog';
@@ -0,0 +1,150 @@
import {
Dialog,
FormGroup,
Input,
InputError,
Intent,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useProfileDialogStore } from '../../stores/profile-dialog-store';
import { useForm } from 'react-hook-form';
import { useT } from '../../lib/use-t';
import { useRequired } from '@vegaprotocol/utils';
import {
useSimpleTransaction,
type Status,
useVegaWallet,
} from '@vegaprotocol/wallet-react';
import {
usePartyProfilesQuery,
type PartyProfilesQuery,
} from '../vega-wallet-connect-button/__generated__/PartyProfiles';
export const ProfileDialog = () => {
const t = useT();
const { pubKeys } = useVegaWallet();
const { data, refetch } = usePartyProfilesQuery({
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
skip: pubKeys.length <= 0,
});
const open = useProfileDialogStore((store) => store.open);
const pubKey = useProfileDialogStore((store) => store.pubKey);
const setOpen = useProfileDialogStore((store) => store.setOpen);
const { send, status, error, reset } = useSimpleTransaction({
onSuccess: () => {
refetch();
},
});
const profileEdge = data?.partiesProfilesConnection?.edges.find(
(e) => e.node.partyId === pubKey
);
const sendTx = (field: FormFields) => {
send({
updatePartyProfile: {
alias: field.alias,
metadata: [],
},
});
};
return (
<Dialog
open={open}
onChange={() => {
setOpen(undefined);
reset();
}}
title={t('Edit profile')}
>
<ProfileForm
profile={profileEdge?.node}
status={status}
error={error}
onSubmit={sendTx}
/>
</Dialog>
);
};
interface FormFields {
alias: string;
}
type Profile = NonNullable<
PartyProfilesQuery['partiesProfilesConnection']
>['edges'][number]['node'];
const ProfileForm = ({
profile,
onSubmit,
status,
error,
}: {
profile: Profile | undefined;
onSubmit: (fields: FormFields) => void;
status: Status;
error: string | undefined;
}) => {
const t = useT();
const required = useRequired();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
alias: profile?.alias,
},
});
const renderButtonText = () => {
if (status === 'requested') {
return t('Confirm in wallet...');
}
if (status === 'pending') {
return t('Confirming transaction...');
}
return t('Submit');
};
const errorMessage = errors.alias?.message || error;
return (
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
<FormGroup label="Alias" labelFor="alias">
<Input
{...register('alias', {
validate: {
required,
},
})}
/>
{errorMessage && (
<InputError>
<p className="break-words max-w-full first-letter:uppercase">
{errorMessage}
</p>
</InputError>
)}
{status === 'confirmed' && (
<p className="mt-2 mb-4 text-sm text-success">
{t('Profile updated')}
</p>
)}
</FormGroup>
<TradingButton
type="submit"
intent={Intent.Info}
disabled={status === 'requested' || status === 'pending'}
>
{renderButtonText()}
</TradingButton>
</form>
);
};
@@ -0,0 +1,14 @@
query PartyProfiles($partyIds: [ID!]) {
partiesProfilesConnection(ids: $partyIds) {
edges {
node {
partyId
alias
metadata {
key
value
}
}
}
}
}
@@ -0,0 +1,57 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type PartyProfilesQueryVariables = Types.Exact<{
partyIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
export type PartyProfilesQuery = { __typename?: 'Query', partiesProfilesConnection?: { __typename?: 'PartiesProfilesConnection', edges: Array<{ __typename?: 'PartyProfileEdge', node: { __typename?: 'PartyProfile', partyId: string, alias: string, metadata: Array<{ __typename?: 'Metadata', key: string, value: string }> } }> } | null };
export const PartyProfilesDocument = gql`
query PartyProfiles($partyIds: [ID!]) {
partiesProfilesConnection(ids: $partyIds) {
edges {
node {
partyId
alias
metadata {
key
value
}
}
}
}
}
`;
/**
* __usePartyProfilesQuery__
*
* To run a query within a React component, call `usePartyProfilesQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyProfilesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = usePartyProfilesQuery({
* variables: {
* partyIds: // value for 'partyIds'
* },
* });
*/
export function usePartyProfilesQuery(baseOptions?: Apollo.QueryHookOptions<PartyProfilesQuery, PartyProfilesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<PartyProfilesQuery, PartyProfilesQueryVariables>(PartyProfilesDocument, options);
}
export function usePartyProfilesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PartyProfilesQuery, PartyProfilesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<PartyProfilesQuery, PartyProfilesQueryVariables>(PartyProfilesDocument, options);
}
export type PartyProfilesQueryHookResult = ReturnType<typeof usePartyProfilesQuery>;
export type PartyProfilesLazyQueryHookResult = ReturnType<typeof usePartyProfilesLazyQuery>;
export type PartyProfilesQueryResult = Apollo.QueryResult<PartyProfilesQuery, PartyProfilesQueryVariables>;
@@ -1,21 +1,57 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { VegaWalletConnectButton } from './vega-wallet-connect-button';
import { truncateByChars } from '@vegaprotocol/utils';
import userEvent from '@testing-library/user-event';
import {
mockConfig,
MockedWalletProvider,
} from '@vegaprotocol/wallet-react/testing';
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import {
PartyProfilesDocument,
type PartyProfilesQuery,
} from './__generated__/PartyProfiles';
jest.mock('../../lib/hooks/use-get-current-route-id', () => ({
useGetCurrentRouteId: jest.fn().mockReturnValue('current-route-id'),
}));
const key = { publicKey: '123456__123456', name: 'test' };
const key2 = { publicKey: 'abcdef__abcdef', name: 'test2' };
const keys = [key, key2];
const keyProfile = {
__typename: 'PartyProfile' as const,
partyId: key.publicKey,
alias: `${key.name} alias`,
metadata: [],
};
const renderComponent = (mockOnClick = jest.fn()) => {
const partyProfilesMock: MockedResponse<PartyProfilesQuery> = {
request: {
query: PartyProfilesDocument,
variables: { partyIds: keys.map((k) => k.publicKey) },
},
result: {
data: {
partiesProfilesConnection: {
__typename: 'PartiesProfilesConnection',
edges: [
{
__typename: 'PartyProfileEdge',
node: keyProfile,
},
],
},
},
},
};
return (
<MockedWalletProvider>
<VegaWalletConnectButton onClick={mockOnClick} />
</MockedWalletProvider>
<MockedProvider mocks={[partyProfilesMock]}>
<MockedWalletProvider>
<VegaWalletConnectButton onClick={mockOnClick} />
</MockedWalletProvider>
</MockedProvider>
);
};
@@ -43,10 +79,6 @@ describe('VegaWalletConnectButton', () => {
});
it('should open dropdown and refresh keys when connected', async () => {
const key = { publicKey: '123456__123456', name: 'test' };
const key2 = { publicKey: 'abcdef__abcdef', name: 'test2' };
const keys = [key, key2];
mockConfig.store.setState({
status: 'connected',
keys,
@@ -61,14 +93,22 @@ describe('VegaWalletConnectButton', () => {
expect(screen.queryByTestId('connect-vega-wallet')).not.toBeInTheDocument();
const button = screen.getByTestId('manage-vega-wallet');
expect(button).toHaveTextContent(truncateByChars(key.publicKey));
expect(button).toHaveTextContent(key.name);
fireEvent.click(button);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(await screen.findAllByRole('menuitemradio')).toHaveLength(
keys.length
const menuItems = await screen.findAllByRole('menuitemradio');
expect(menuItems).toHaveLength(keys.length);
expect(within(menuItems[0]).getByTestId('alias')).toHaveTextContent(
keyProfile.alias
);
expect(within(menuItems[1]).getByTestId('alias')).toHaveTextContent(
'No alias'
);
expect(refreshKeys).toHaveBeenCalled();
fireEvent.click(screen.getByTestId(`key-${key2.publicKey}`));
@@ -14,6 +14,7 @@ import {
TradingDropdownItem,
TradingDropdownRadioItem,
TradingDropdownItemIndicator,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { isBrowserWalletInstalled, type Key } from '@vegaprotocol/wallet';
import { useDialogStore, useVegaWallet } from '@vegaprotocol/wallet-react';
@@ -22,6 +23,8 @@ import classNames from 'classnames';
import { ViewType, useSidebar } from '../sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { usePartyProfilesQuery } from './__generated__/PartyProfiles';
import { useProfileDialogStore } from '../../stores/profile-dialog-store';
export const VegaWalletConnectButton = ({
intent = Intent.None,
@@ -68,10 +71,10 @@ export const VegaWalletConnectButton = ({
{activeKey ? (
<>
{activeKey && (
<span className="uppercase">{activeKey.name}</span>
<span className="uppercase">
{activeKey.name ? activeKey.name : t('Unnamed key')}
</span>
)}
{' | '}
{truncateByChars(activeKey.publicKey)}
</>
) : (
<>{'Select key'}</>
@@ -88,20 +91,11 @@ export const VegaWalletConnectButton = ({
onEscapeKeyDown={() => setDropdownOpen(false)}
>
<div className="min-w-[340px]" data-testid="keypair-list">
<TradingDropdownRadioGroup
value={pubKey || undefined}
onValueChange={(value) => {
selectPubKey(value);
}}
>
{pubKeys.map((pk) => (
<KeypairItem
key={pk.publicKey}
pk={pk}
active={pk.publicKey === pubKey}
/>
))}
</TradingDropdownRadioGroup>
<KeypairRadioGroup
pubKey={pubKey}
pubKeys={pubKeys}
onSelect={selectPubKey}
/>
<TradingDropdownSeparator />
{!isReadOnly && (
<TradingDropdownItem
@@ -141,28 +135,52 @@ export const VegaWalletConnectButton = ({
);
};
const KeypairItem = ({ pk, active }: { pk: Key; active: boolean }) => {
const KeypairRadioGroup = ({
pubKey,
pubKeys,
onSelect,
}: {
pubKey: string | undefined;
pubKeys: Key[];
onSelect: (pubKey: string) => void;
}) => {
const { data } = usePartyProfilesQuery({
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
skip: pubKeys.length <= 0,
});
return (
<TradingDropdownRadioGroup value={pubKey} onValueChange={onSelect}>
{pubKeys.map((pk) => {
const profile = data?.partiesProfilesConnection?.edges.find(
(e) => e.node.partyId === pk.publicKey
);
return (
<KeypairItem key={pk.publicKey} pk={pk} alias={profile?.node.alias} />
);
})}
</TradingDropdownRadioGroup>
);
};
const KeypairItem = ({ pk, alias }: { pk: Key; alias: string | undefined }) => {
const t = useT();
const [copied, setCopied] = useCopyTimeout();
const setOpen = useProfileDialogStore((store) => store.setOpen);
return (
<TradingDropdownRadioItem value={pk.publicKey}>
<div
className={classNames('flex-1 mr-2', {
'text-default': active,
'text-muted': !active,
})}
data-testid={`key-${pk.publicKey}`}
>
<span className={classNames('mr-2 uppercase')}>
{pk.name}
<div>
<div className="flex items-center gap-2">
<span>{pk.name ? pk.name : t('Unnamed key')}</span>
{' | '}
{truncateByChars(pk.publicKey)}
</span>
<span className="inline-flex items-center gap-1">
<span className="font-mono">
{truncateByChars(pk.publicKey, 3, 3)}
</span>
<CopyToClipboard text={pk.publicKey} onCopy={() => setCopied(true)}>
<button
data-testid="copy-vega-public-key"
className="relative -top-px"
onClick={(e) => e.stopPropagation()}
>
<span className="sr-only">{t('Copy')}</span>
@@ -170,7 +188,17 @@ const KeypairItem = ({ pk, active }: { pk: Key; active: boolean }) => {
</button>
</CopyToClipboard>
{copied && <span className="text-xs">{t('Copied')}</span>}
</span>
</div>
<div
className={classNames('flex-1 mr-2 text-secondary text-sm')}
data-testid={`key-${pk.publicKey}`}
>
<Tooltip description={t('Public facing key alias. Click to edit')}>
<button data-testid="alias" onClick={() => setOpen(pk.publicKey)}>
{alias ? alias : t('No alias')}
</button>
</Tooltip>
</div>
</div>
<TradingDropdownItemIndicator />
</TradingDropdownRadioItem>
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.74.6
VEGA_VERSION=v0.75.0-preview.2
LOCAL_SERVER=false
+4 -4
View File
@@ -877,13 +877,13 @@ testing = ["filelock"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
]
[package.dependencies]
@@ -1166,7 +1166,7 @@ profile = ["pytest-profiling", "snakeviz"]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git/"
reference = "HEAD"
resolved_reference = "33fec45ce8044ef7f53b625584ce590d174f9057"
resolved_reference = "53eed8942acb670783105cb1115bab76710a46dc"
[[package]]
name = "websocket-client"
@@ -37,19 +37,16 @@ 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%"],
@@ -61,8 +58,7 @@ 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"],
@@ -71,11 +67,10 @@ def test_market_info_market_price(page: Page):
]
validate_info_section(page, fields)
def test_market_info_market_volume(page: Page):
#TODO: remove skip once volume is fixed
""" 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"],
@@ -84,12 +79,13 @@ def test_market_info_market_volume(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"],
@@ -101,16 +97,14 @@ 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 = [
@@ -156,8 +150,7 @@ 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 = [
@@ -211,8 +204,7 @@ 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"],
@@ -232,8 +224,7 @@ 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"],
@@ -254,7 +245,7 @@ def test_market_info_liquidity_monitoring_parameters(page: Page):
# Liquidity resolves to 3 results
def test_market_info_liquidit(page: Page):
def test_market_info_liquidity(page: Page):
# 6002-MDET-213
page.get_by_test_id(market_title_test_id).get_by_text(
"Liquidity", exact=True
@@ -283,17 +274,14 @@ 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")
@@ -304,12 +292,10 @@ 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})")
)
@@ -15,13 +15,13 @@ def test_market_selector(continuous_market, page: Page):
# 6001-MARK-025
btc_market = page.locator('[data-testid="market-selector-list"] a')
expect(btc_market.locator("h3")).to_have_text("BTC:DAI_2023Futr")
expect(btc_market.locator('[data-testid="market-selector-volume"]')).to_have_text(
"1"
)
# tbd - 5465
# expect(btc_market.locator('[data-testid="market-selector-volume"]')).to_have_text(
# "1"
# )
expect(btc_market.locator('[data-testid="market-selector-price"]')).to_have_text(
"107.50 tDAI"
)
expect(btc_market.locator("span.rounded-md.leading-none")).to_be_visible()
expect(btc_market.locator("span.rounded-md.leading-none")).to_have_text("Futr")
expect(btc_market.locator(
'[data-testid="sparkline-svg"]')).not_to_be_visible
expect(btc_market.locator('[data-testid="sparkline-svg"]')).not_to_be_visible
@@ -57,7 +57,7 @@ class TestPerpetuals:
page.goto(f"/#/markets/{perps_market}")
page.get_by_test_id("Funding payments").click()
row = page.locator(row_selector)
expect(row.locator(col_amount)).to_have_text("9.00 tDAI")
expect(row.locator(col_amount)).to_have_text("4.45 tDAI")
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_funding_payment_loss(self, perps_market, page: Page, vega):
@@ -65,7 +65,7 @@ class TestPerpetuals:
change_keys(page, vega, "market_maker")
page.get_by_test_id("Funding payments").click()
row = page.locator(row_selector)
expect(row.locator(col_amount)).to_have_text("-27.00 tDAI")
expect(row.locator(col_amount)).to_have_text("-13.35 tDAI")
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_funding_header(self, perps_market, page: Page):
@@ -97,10 +97,13 @@ 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()
+3 -3
View File
@@ -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(2)
expect(competitions_page.get_by_test_id("active-rewards-card")).to_have_count(1)
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("Individual")
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Team")
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("In team")
expect(game_1.get_by_test_id("scope")).to_have_text("All teams")
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")
+6 -2
View File
@@ -25,8 +25,12 @@ fragment GameFields on Game {
}
}
query Games($epochFrom: Int) {
games(epochFrom: $epochFrom, entityScope: ENTITY_SCOPE_TEAMS) {
query Games($epochFrom: Int, $teamId: ID) {
games(
epochFrom: $epochFrom
teamId: $teamId
entityScope: ENTITY_SCOPE_TEAMS
) {
edges {
node {
...GameFields
+4 -2
View File
@@ -9,6 +9,7 @@ export type GameFieldsFragment = { __typename?: 'Game', id: string, epoch: numbe
export type GamesQueryVariables = Types.Exact<{
epochFrom?: Types.InputMaybe<Types.Scalars['Int']>;
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
@@ -44,8 +45,8 @@ export const GameFieldsFragmentDoc = gql`
}
${TeamEntityFragmentDoc}`;
export const GamesDocument = gql`
query Games($epochFrom: Int) {
games(epochFrom: $epochFrom, entityScope: ENTITY_SCOPE_TEAMS) {
query Games($epochFrom: Int, $teamId: ID) {
games(epochFrom: $epochFrom, teamId: $teamId, entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...GameFields
@@ -68,6 +69,7 @@ export const GamesDocument = gql`
* const { data, loading, error } = useGamesQuery({
* variables: {
* epochFrom: // value for 'epochFrom'
* teamId: // value for 'teamId'
* },
* });
*/
+1
View File
@@ -51,6 +51,7 @@ export const useGames = (teamId?: string, epochFrom?: number): GamesData => {
const { data, loading, error } = useGamesQuery({
variables: {
epochFrom: from,
teamId: teamId,
},
skip: !from,
fetchPolicy: 'cache-and-network',
+2 -2
View File
@@ -194,11 +194,11 @@ describe('isScopedToTeams', () => {
undefined,
makeDispatchStrategy(
EntityScope.ENTITY_SCOPE_INDIVIDUALS,
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM // individual in teams
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM // individual in teams but not a team game
),
'RecurringTransfer'
),
true,
false,
],
[
makeReward(
+1 -11
View File
@@ -14,7 +14,6 @@ import {
TransferStatus,
type DispatchStrategy,
EntityScope,
IndividualScope,
MarketState,
AccountType,
} from '@vegaprotocol/types';
@@ -75,20 +74,11 @@ 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 ||
// 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);
EntityScope.ENTITY_SCOPE_TEAMS;
/** Retrieves rewards (transfers) */
export const useRewards = ({
-2
View File
@@ -24,7 +24,6 @@ import {
ProtocolUpgradeInProgressNotification,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner';
import { Telemetry } from '../components/telemetry';
import { SSRLoader } from './ssr-loader';
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
@@ -77,7 +76,6 @@ function AppBody({ Component }: AppProps) {
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ProtocolUpgradeInProgressNotification />
<ViewingBanner />
</div>
<div data-testid={`pathname-${location.pathname}`}>
<Component />
+2
View File
@@ -8,6 +8,7 @@ import {
} from '@vegaprotocol/web3';
import { WelcomeDialog } from '../components/welcome-dialog';
import { VegaWalletConnectDialog } from '../components/vega-wallet-connect-dialog';
import { ProfileDialog } from '../components/profile-dialog';
const DialogsContainer = () => {
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
@@ -24,6 +25,7 @@ const DialogsContainer = () => {
<WelcomeDialog />
<Web3ConnectUncontrolledDialog />
<WithdrawalApprovalDialogContainer />
<ProfileDialog />
</>
);
};
@@ -0,0 +1,19 @@
import { create } from 'zustand';
interface ProfileDialogStore {
open: boolean;
pubKey: string | undefined;
setOpen: (pubKey: string | undefined) => void;
}
export const useProfileDialogStore = create<ProfileDialogStore>((set) => ({
open: false,
pubKey: undefined,
setOpen: (pubKey) => {
if (pubKey) {
set({ open: true, pubKey });
} else {
set({ open: false, pubKey: undefined });
}
},
}));
+5
View File
@@ -6,7 +6,12 @@ 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;
+45
View File
@@ -34,18 +34,28 @@ 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,
],
@@ -137,10 +147,15 @@ 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,
],
@@ -255,6 +270,10 @@ const getDuration = (
multiplier: number
): Duration => {
switch (interval) {
case 'I7D':
return {
days: 7 * multiplier,
};
case 'I1D':
return {
days: 1 * multiplier,
@@ -271,14 +290,30 @@ 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,
};
}
};
@@ -288,14 +323,24 @@ 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={(orderSubmission) => create({ orderSubmission })}
submit={(transaction) => create(transaction)}
/>
)}
</>
@@ -0,0 +1,172 @@
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 another')}</span>}
description={<span>{t('One cancels the other')}</span>}
>
<>{t('OCO')}</>
</Tooltip>
@@ -8,9 +8,12 @@ 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 { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
import { type Transaction } from '@vegaprotocol/wallet';
import {
mapFormValuesToOrderSubmission,
mapFormValuesToTakeProfitAndStopLoss,
} from '../../utils/map-form-values-to-submission';
import {
TradingInput as Input,
TradingCheckbox as Checkbox,
@@ -77,6 +80,8 @@ 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.';
@@ -86,7 +91,7 @@ export interface DealTicketProps {
marketData: StaticMarketData;
marketPrice?: string | null;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
submit: (order: OrderSubmission) => void;
submit: (order: Transaction) => void;
onDeposit: (assetId: string) => void;
}
@@ -184,6 +189,7 @@ 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');
@@ -382,17 +388,28 @@ export const DealTicket = ({
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
return;
}
submit(
mapFormValuesToOrderSubmission(
if (formValues.tpSl) {
const reference = `${pubKey}-${now}-${uniqueId()}`;
const batchMarketInstructions = mapFormValuesToTakeProfitAndStopLoss(
formValues,
market,
reference
);
submit({
batchMarketInstructions,
});
} else {
const orderSubmission = mapFormValuesToOrderSubmission(
formValues,
market.id,
market.decimalPlaces,
market.positionDecimalPlaces
)
);
);
submit({ orderSubmission });
}
lastSubmitTime.current = now;
},
[submit, market.decimalPlaces, market.positionDecimalPlaces, market.id]
[market, pubKey, submit]
);
useController({
name: 'type',
@@ -674,40 +691,63 @@ 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="iceberg"
name="tpSl"
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>
<p>{t('TP_SL_TOOLTIP', 'Take profit / Stop loss')}</p>
}
>
<div>
<Checkbox
name="iceberg"
name="tpSl"
checked={field.value}
onCheckedChange={field.onChange}
disabled={disableIcebergCheckbox}
label={t('Iceberg')}
disabled={false}
label={t('TP / SL')}
/>
</div>
</Tooltip>
)}
/>
</div>
{iceberg && (
{isLimitType && iceberg && (
<DealTicketSizeIceberg
market={market}
peakSizeError={errors.peakSize?.message}
@@ -717,8 +757,18 @@ 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,6 +53,9 @@ export type OrderFormValues = {
iceberg?: boolean;
peakSize?: string;
minimumVisibleSize?: string;
tpSl?: boolean;
takeProfit?: string;
stopLoss?: string;
};
type UpdateOrder = (marketId: string, values: Partial<OrderFormValues>) => void;
@@ -10,13 +10,16 @@ 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
positionDecimalPlaces: number,
reference?: string
): OrderSubmission => ({
reference,
marketId: marketId,
type: order.type,
side: order.side,
@@ -81,7 +84,8 @@ export const mapFormValuesToStopOrdersSubmission = (
data: StopOrderFormValues,
marketId: string,
decimalPlaces: number,
positionDecimalPlaces: number
positionDecimalPlaces: number,
reference?: string
): StopOrdersSubmission => {
const submission: StopOrdersSubmission = {};
const stopOrderSetup: StopOrderSetup = {
@@ -96,7 +100,8 @@ export const mapFormValuesToStopOrdersSubmission = (
},
marketId,
decimalPlaces,
positionDecimalPlaces
positionDecimalPlaces,
reference
),
};
setTrigger(
@@ -120,7 +125,8 @@ export const mapFormValuesToStopOrdersSubmission = (
},
marketId,
decimalPlaces,
positionDecimalPlaces
positionDecimalPlaces,
reference
),
};
setTrigger(
@@ -159,3 +165,125 @@ 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,8 +1,15 @@
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
import type {
OrderSubmissionBody,
StopOrdersSubmission,
} from '@vegaprotocol/wallet';
import {
mapFormValuesToOrderSubmission,
mapFormValuesToTakeProfitAndStopLoss,
} 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', () => {
@@ -186,3 +193,232 @@ 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);
});
});
+7 -1
View File
@@ -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 another": "One cancels another",
"One cancels the other": "One cancels the other",
"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,6 +135,12 @@
"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",
+18 -3
View File
@@ -86,6 +86,7 @@
"Docs": "Docs",
"Earn commission & stake rewards": "Earn commission & stake rewards",
"Earned by me": "Earned by me",
"Edit alias": "Edit alias",
"Eligible teams": "Eligible teams",
"Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass",
"[empty]": "[empty]",
@@ -142,12 +143,17 @@
"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": "1D",
"INTERVAL_I1H": "1H",
"INTERVAL_I1D": "D",
"INTERVAL_I1H": "1h",
"INTERVAL_I1M": "1m",
"INTERVAL_I30M": "30m",
"INTERVAL_I4H": "4H",
"INTERVAL_I5M": "5m",
"INTERVAL_I6H": "6H",
"INTERVAL_I6H": "6h",
"INTERVAL_I8H": "8h",
"INTERVAL_I7D": "W",
"Improve vega console": "Improve vega console",
"Inactive": "Inactive",
"Index Price": "Index Price",
@@ -162,6 +168,7 @@
"Joined": "Joined",
"Joined at": "Joined at",
"Joined epoch": "Joined epoch",
"Key name": "Key name",
"gameCount_one": "Last game result",
"gameCount_other": "Last {{count}} game results",
"Learn about providing liquidity": "Learn about providing liquidity",
@@ -194,6 +201,7 @@
"My liquidity provision": "My liquidity provision",
"My trading fees": "My trading fees",
"Name": "Name",
"No alias": "No alias",
"No closed orders": "No closed orders",
"No data": "No data",
"No deposits": "No deposits",
@@ -227,6 +235,7 @@
"Not connected": "Not connected",
"Number of epochs after distribution to delay vesting of rewards by": "Number of epochs after distribution to delay vesting of rewards by",
"Number of traders": "Number of traders",
"On-change alias": "On-change alias",
"Open": "Open",
"Open a position": "Open a position",
"Open markets": "Open markets",
@@ -249,6 +258,7 @@
"Portfolio": "Portfolio",
"Positions": "Positions",
"Price": "Price",
"Profile updated": "Profile updated",
"Program ends:": "Program ends:",
"Propose a new market": "Propose a new market",
"Proposed final price is {{price}} {{assetSymbol}}.": "Proposed final price is {{price}} {{assetSymbol}}.",
@@ -289,6 +299,7 @@
"Search": "Search",
"See all markets": "See all markets",
"Select market": "Select market",
"Set party alias": "Set party alias",
"Settings": "Settings",
"Settlement asset": "Settlement asset",
"Settlement date": "Settlement date",
@@ -311,6 +322,7 @@
"Stop": "Stop",
"Stop orders": "Stop orders",
"Streak reward multiplier": "Streak reward multiplier",
"Submit": "Submit",
"Successor of a market": "Successor of a market",
"Successors to this market have been proposed": "Successors to this market have been proposed",
"Supplied stake": "Supplied stake",
@@ -371,6 +383,7 @@
"Staking rewards": "Staking rewards",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"Unnamed key": "Unnamed key",
"Update team": "Update team",
"URL": "URL",
"Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
@@ -407,8 +420,10 @@
"You will no longer be able to hold a position on this market when it closes in {{duration}}.": "You will no longer be able to hold a position on this market when it closes in {{duration}}.",
"Your code has been rejected": "Your code has been rejected",
"Your identity is always anonymous on Vega": "Your identity is always anonymous on Vega",
"Your key's private name, can be changed in your wallet": "Your key's private name, can be changed in your wallet",
"Your referral code": "Your referral code",
"Your tier": "Your tier",
"Your public alias, stored on chain": "Your public alias, stored on chain",
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
+5
View File
@@ -15,9 +15,14 @@ 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,9 +32,14 @@ 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);
+12
View File
@@ -936,6 +936,8 @@ export enum DispatchMetric {
/** Dispatch strategy for a recurring transfer */
export type DispatchStrategy = {
__typename?: 'DispatchStrategy';
/** Optional multiplier on taker fees used to cap the rewards a party may receive in an epoch */
capRewardFeeMultiple?: Maybe<Scalars['String']>;
/** Defines the data that will be used to compare markets so as to distribute rewards appropriately */
dispatchMetric: DispatchMetric;
/** The asset to use for measuring contribution to the metric */
@@ -2391,6 +2393,8 @@ export type Market = {
state: MarketState;
/** Optional: Market ID of the successor to this market if one exists */
successorMarketID?: Maybe<Scalars['ID']>;
/** The market minimum tick size */
tickSize: Scalars['String'];
/** An instance of, or reference to, a tradable instrument. */
tradableInstrument: TradableInstrument;
/** @deprecated Simplify and consolidate trades query and remove nesting. Use trades query instead */
@@ -2816,6 +2820,8 @@ export type NewMarket = {
riskParameters: RiskModel;
/** Successor market configuration. If this proposed market is meant to succeed a given market, then this needs to be set. */
successorConfiguration?: Maybe<SuccessorConfiguration>;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
/** Configuration for a new spot market on Vega */
@@ -2839,6 +2845,8 @@ export type NewSpotMarket = {
riskParameters?: Maybe<RiskModel>;
/** Specifies parameters related to liquidity target stake calculation */
targetStakeParameters: TargetStakeParameters;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
export type NewTransfer = {
@@ -7074,6 +7082,8 @@ export type UpdateMarketConfiguration = {
quadraticSlippageFactor: Scalars['String'];
/** Updated futures market risk model parameters. */
riskParameters: UpdateMarketRiskParameters;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
export type UpdateMarketLogNormalRiskModel = {
@@ -7171,6 +7181,8 @@ export type UpdateSpotMarketConfiguration = {
riskParameters: RiskModel;
/** Specifies parameters related to target stake calculation */
targetStakeParameters: TargetStakeParameters;
/** The market minimum tick size */
tickSize: Scalars['String'];
};
export type UpdateVolumeDiscountProgram = {
@@ -33,6 +33,12 @@ export const useSimpleTransaction = (opts?: Options) => {
const [result, setResult] = useState<Result>();
const [error, setError] = useState<string>();
const reset = () => {
setStatus('idle');
setResult(undefined);
setError(undefined);
};
const send = async (tx: Transaction) => {
if (!pubKey) {
throw new Error('no pubKey');
@@ -114,5 +120,6 @@ export const useSimpleTransaction = (opts?: Options) => {
error,
status,
send,
reset,
};
};
+13 -1
View File
@@ -403,6 +403,9 @@ 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[];
};
}
@@ -492,6 +495,14 @@ export interface UpdateMarginMode {
export interface UpdateMarginModeBody {
updateMarginMode: UpdateMarginMode;
}
export interface UpdatePartyProfile {
updatePartyProfile: {
alias: string;
metadata: Array<{ key: string; value: string }>;
};
}
export type Transaction =
| UpdateMarginModeBody
| StopOrdersSubmissionBody
@@ -510,7 +521,8 @@ export type Transaction =
| ApplyReferralCode
| JoinTeam
| CreateReferralSet
| UpdateReferralSet;
| UpdateReferralSet
| UpdatePartyProfile;
export interface TransactionResponse {
transactionHash: string;
+1 -2
View File
@@ -11,7 +11,6 @@
"build:all": "nx run-many --all --target=build",
"build-spec:all": "nx run-many --all --target=build-spec",
"lint:all": "nx run-many --all --target=lint",
"e2e:all": "nx run-many --all --target=e2e",
"vegacapsule": "vegacapsule network bootstrap --config-path=../frontend-monorepo/vegacapsule/config.hcl",
"release": "git checkout develop ; git pull ; node scripts/make-release.js",
"trading:test": "cd apps/trading/e2e && poetry run pytest -k",
@@ -77,7 +76,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "^1.15.0",
"pennant": "^1.16.2",
"react": "18.2.0",
"react-copy-to-clipboard": "5.1.0",
"react-dom": "18.2.0",
+43 -37
View File
@@ -7,18 +7,18 @@ projects = []
projects_e2e = []
previews = {
'governance': 'not deployed',
'explorer': 'not deployed',
'trading': 'not deployed',
'tools': 'not deployed',
'governance': 'not deployed',
'explorer': 'not deployed',
'trading': 'not deployed',
'tools': 'not deployed',
}
main_apps = ['governance', 'explorer', 'trading']
preview_governance="not deployed"
preview_trading="not deployed"
preview_explorer="not deployed"
preview_tools="not deployed"
preview_governance = "not deployed"
preview_trading = "not deployed"
preview_explorer = "not deployed"
preview_tools = "not deployed"
# take input from the pipeline
parser = ArgumentParser()
@@ -30,7 +30,8 @@ parser.add_argument('--event-name', help='name of event in CI')
args = parser.parse_args()
# run yarn affected command
affected=check_output(f'yarn nx print-affected --base={environ["NX_BASE"]} --head={environ["NX_HEAD"]} --select=projects'.split()).decode('utf-8')
affected = check_output(
f'yarn nx print-affected --base={environ["NX_BASE"]} --head={environ["NX_HEAD"]} --select=projects'.split()).decode('utf-8')
# print useful information
@@ -44,49 +45,54 @@ print(affected)
print(">>>> eof debug")
# define affection actions -> add to projects arrays and generate preview link
def affect_app(app, preview_name=None):
print(f"{app} is affected")
projects.append(app)
if not preview_name:
preview_name=app
previews[app] = f'https://{preview_name}.{args.branch_slug}.vega.rocks'
print(f"{app} is affected")
projects.append(app)
if not preview_name:
preview_name = app
previews[app] = f'https://{preview_name}.{args.branch_slug}.vega.rocks'
# check appearance in the affected string for main apps
for app in main_apps:
if app in affected:
affect_app(app)
if app in affected:
affect_app(app)
# if non of main apps is affected - test all of them
if not projects:
for app in main_apps:
affect_app(app)
for app in main_apps:
affect_app(app)
# generate e2e targets
projects_e2e = [f'{app}-e2e' for app in projects]
# remove trading-e2e because it doesn't exists any more (new target is: console-e2e)
if "trading-e2e" in projects_e2e:
projects_e2e.remove("trading-e2e")
# check affection for multisig-signer which is deployed only from develop and pull requests
if args.event_name == 'pull_request' or 'develop' in args.github_ref:
if 'multisig-signer' in affected:
affect_app('multisig-signer', 'tools')
if 'multisig-signer' in affected:
affect_app('multisig-signer', 'tools')
# now parse apps that are deployed from develop but don't have previews
if 'develop' in args.github_ref:
for app in ['static', 'ui-toolkit']:
if app in affected:
projects.append(app)
for app in ['static', 'ui-toolkit']:
if app in affected:
projects.append(app)
# if ref is in format release/{env}-{app} then only {app} is deployed
if 'release' in args.github_ref:
for app in main_apps:
if f'{args.github_ref}'.endswith(app):
projects = [app]
projects_e2e = [f'{app}-e2e']
for app in main_apps:
if f'{args.github_ref}'.endswith(app):
projects = [app]
projects_e2e = [f'{app}-e2e']
projects = json.dumps(projects)
# The trading project does not use the deafult NX e2e setup (cypress)
projects_e2e.remove('trading-e2e')
projects_e2e = json.dumps(projects_e2e)
print(f'Projects: {projects}')
@@ -94,20 +100,20 @@ print(f'Projects E2E: {projects_e2e}')
print('>> Previews')
for preview, preview_value in previews.items():
print(f'{preview}: {preview_value}')
print(f'{preview}: {preview_value}')
print('>> EOF Previews')
lines_to_write = [
f'PREVIEW_GOVERNANCE={previews["governance"]}',
f'PREVIEW_EXPLORER={previews["explorer"]}',
f'PREVIEW_TRADING={previews["trading"]}',
f'PREVIEW_TOOLS={previews["tools"]}',
f'PROJECTS={projects}',
f'PROJECTS_E2E={projects_e2e}',
f'PREVIEW_GOVERNANCE={previews["governance"]}',
f'PREVIEW_EXPLORER={previews["explorer"]}',
f'PREVIEW_TRADING={previews["trading"]}',
f'PREVIEW_TOOLS={previews["tools"]}',
f'PROJECTS={projects}',
f'PROJECTS_E2E={projects_e2e}',
]
env_file = environ['GITHUB_ENV']
print(f'Line to add to GITHUB_ENV file: {env_file}')
print(lines_to_write)
with open(env_file, 'a') as _f:
_f.write('\n'.join(lines_to_write))
_f.write('\n'.join(lines_to_write))
+138 -44
View File
@@ -1460,13 +1460,20 @@
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.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":
"@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":
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"
@@ -2438,14 +2445,22 @@
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.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==
"@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==
dependencies:
"@floating-ui/utils" "^0.1.3"
"@floating-ui/utils" "^0.2.1"
"@floating-ui/dom@^1.2.1", "@floating-ui/dom@^1.5.1":
"@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":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
@@ -2472,6 +2487,11 @@
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"
@@ -7024,9 +7044,9 @@
integrity sha512-5a21DF7avVPmiUau8KTsv5r76yGqbMgq4QtByoCBPXUrVFWFkd3Ob4OOhmePNRbQqfUCNFjgB4sO7sUURnKcBg==
"@types/d3-shape@^2.0.0":
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==
version "2.1.7"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.7.tgz#7c3bd6a9c758b54ba495cab0575cb18359251123"
integrity sha512-HedHlfGHdwzKqX9+PiQVXZrdmGlwo7naoefJP7kCNk4Y7qcpQt1tUaoRa6qn0kbTdlaIHGO7111qLtb/6J8uuw==
dependencies:
"@types/d3-path" "^2"
@@ -7300,11 +7320,16 @@
dependencies:
"@types/node" "*"
"@types/lodash@^4.14.167", "@types/lodash@^4.14.168", "@types/lodash@^4.14.171":
"@types/lodash@^4.14.167", "@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"
@@ -7391,7 +7416,14 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.61.tgz#5ea47e3018348bf3bbbe646b396ba5e720310be1"
integrity sha512-k0N7BqGhJoJzdh6MuQg1V1ragJiXTh8VUBAZTWjJ9cUq23SG0F0xavOwZbhiP4J3y20xd6jxKx+xNUhkMAi76Q==
"@types/node@^18.0.0", "@types/node@^18.17.5":
"@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":
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==
@@ -7425,7 +7457,12 @@
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.3.tgz#47fe8e784c2dee24fe636cab82e090d3da9b7dec"
integrity sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==
"@types/prop-types@*", "@types/prop-types@^15.0.0":
"@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":
version "15.7.10"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.10.tgz#892afc9332c4d62a5ea7e897fe48ed2085bbb08a"
integrity sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==
@@ -7454,13 +7491,20 @@
dependencies:
"@types/react" "*"
"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.5":
"@types/react-dom@^18.0.0":
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"
@@ -7485,7 +7529,14 @@
dependencies:
"@types/react" "*"
"@types/react-virtualized-auto-sizer@^1.0.0", "@types/react-virtualized-auto-sizer@^1.0.1":
"@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":
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==
@@ -7507,10 +7558,10 @@
dependencies:
"@types/react" "*"
"@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==
"@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==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -7525,6 +7576,15 @@
"@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"
@@ -7545,9 +7605,9 @@
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/scheduler@*":
version "0.16.6"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.6.tgz#eb26db6780c513de59bee0b869ef289ad3068711"
integrity sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==
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==
"@types/semver@^7.3.12", "@types/semver@^7.3.4", "@types/semver@^7.5.0":
version "7.5.5"
@@ -9999,7 +10059,22 @@ 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", chokidar@^3.5.2, chokidar@^3.5.3:
"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:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -10051,11 +10126,16 @@ 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.2.6, classnames@^2.3.0, classnames@^2.3.1:
classnames@*, classnames@^2.2, classnames@^2.2.5, 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"
@@ -10890,9 +10970,9 @@ cssstyle@^2.3.0:
cssom "~0.3.6"
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
cypress-mochawesome-reporter@^3.3.0:
version "3.6.1"
@@ -11400,11 +11480,11 @@ del@^6.0.0:
slash "^3.0.0"
delaunator@5:
version "5.0.0"
resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
version "5.0.1"
resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278"
integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==
dependencies:
robust-predicates "^3.0.0"
robust-predicates "^3.0.2"
delay@^5.0.0:
version "5.0.0"
@@ -14314,9 +14394,9 @@ immer@^9.0.12:
integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
immutable@^4.0.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
version "4.3.5"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0"
integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==
immutable@~3.7.6:
version "3.7.6"
@@ -17903,10 +17983,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.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.15.0.tgz#21854cf78466cbd27eda8143c21abcde070d4d76"
integrity sha512-p3H4vu6BP7nUqn7s2pjyTl0FMJUT2U5lq5UKuxy55F2E66sJnvp5XFvNjRZvi42vyzsxG8WURgwQDQKNqQgWAw==
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==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"
@@ -19219,7 +19299,12 @@ 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, react-virtualized-auto-sizer@^1.0.6:
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:
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==
@@ -19430,9 +19515,9 @@ regenerator-runtime@^0.13.7:
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regenerator-transform@^0.15.2:
version "0.15.2"
@@ -19727,7 +19812,7 @@ rimraf@~2.6.2:
dependencies:
glob "^7.1.3"
robust-predicates@^3.0.0:
robust-predicates@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
@@ -19916,7 +20001,7 @@ sass@1.55.0:
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sass@^1.42.1, sass@^1.49.9:
sass@^1.42.1:
version "1.69.5"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde"
integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==
@@ -19925,6 +20010,15 @@ sass@^1.42.1, sass@^1.49.9:
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"