Compare commits

..
49 changed files with 781 additions and 842 deletions
+3
View File
@@ -1,5 +1,8 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Auto-format all files
yarn nx format:write
# Lint all staged files
yarn lint-staged
+3 -3
View File
@@ -1,8 +1,8 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files
yarn nx format:check
# Lint all staged files - this brings more value as pre-commit
# yarn nx format:check
# Test all projects with changes
yarn nx affected -t test --exclude trading
# yarn nx affected -t test --exclude trading
+4 -8
View File
@@ -3,17 +3,16 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
@@ -26,6 +25,3 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+2 -3
View File
@@ -22,10 +22,9 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=false
NX_REFERRALS=true
NX_DISABLE_CLOSE_POSITION=true
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_DISABLE_CLOSE_POSITION=true
-1
View File
@@ -23,7 +23,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
@@ -13,15 +13,40 @@ import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links';
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
import { Statistics, useStats } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program';
import { useT } from '../../lib/use-t';
import { ns, useT } from '../../lib/use-t';
import { useFundsAvailable } from './hooks/use-funds-available';
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';
const RELOAD_DELAY = 3000;
const SPAM_PROTECTION_ERR = 'SPAM_PROTECTION_ERR';
const SpamProtectionErr = ({
requiredFunds,
}: {
requiredFunds?: string | number | bigint;
}) => {
if (!requiredFunds) return null;
// eslint-disable-next-line react/jsx-no-undef
return (
<Trans
defaults="To protect the network from spam, you must have at least {{requiredFunds}} <0>qUSD</0> of any asset on the network to proceed."
values={{
requiredFunds,
}}
components={[<QUSDTooltip key="qusd" />]}
ns={ns}
/>
);
};
const validateCode = (value: string, t: ReturnType<typeof useT>) => {
const number = +`0x${value}`;
if (!value || value.length !== 64) {
@@ -32,20 +57,23 @@ const validateCode = (value: string, t: ReturnType<typeof useT>) => {
return true;
};
export const ApplyCodeFormContainer = () => {
export const ApplyCodeFormContainer = ({
onSuccess,
}: {
onSuccess?: () => void;
}) => {
const { pubKey } = useVegaWallet();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
const isInReferralSet = useIsInReferralSet(pubKey);
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
// Navigate to the index page when already in the referral set.
if (isInReferralSet) {
return <Navigate to={Routes.REFERRALS} />;
}
return <ApplyCodeForm />;
return <ApplyCodeForm onSuccess={onSuccess} />;
};
export const ApplyCodeForm = () => {
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
const t = useT();
const program = useReferralProgram();
const navigate = useNavigate();
@@ -54,10 +82,15 @@ export const ApplyCodeForm = () => {
);
const [status, setStatus] = useState<
'requested' | 'failed' | 'successful' | null
'requested' | 'no-funds' | 'successful' | null
>(null);
const txHash = useRef<string | null>(null);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { isEligible, requiredFunds } = useFundsAvailable();
const currentRouteId = useGetCurrentRouteId();
const setViews = useSidebar((s) => s.setViews);
const {
register,
handleSubmit,
@@ -73,6 +106,17 @@ export const ApplyCodeForm = () => {
code: validateCode(codeField, t) ? codeField : undefined,
});
/**
* Validates if a connected party can apply a code (min funds span protection)
*/
const validateFundsAvailable = useCallback(() => {
if (requiredFunds && !isEligible) {
const err = SPAM_PROTECTION_ERR;
return err;
}
return true;
}, [isEligible, requiredFunds]);
/**
* Validates the set a user tries to apply to.
*/
@@ -96,6 +140,15 @@ export const ApplyCodeForm = () => {
if (code) setValue('code', code);
}, [params, setValue]);
useEffect(() => {
const err = validateFundsAvailable();
if (err !== true) {
setStatus('no-funds');
} else {
setStatus(null);
}
}, [isEligible, validateFundsAvailable]);
const onSubmit = ({ code }: FieldValues) => {
if (isReadOnly || !pubKey || !code || code.length === 0) {
return;
@@ -167,10 +220,11 @@ export const ApplyCodeForm = () => {
useEffect(() => {
if (status === 'successful') {
setTimeout(() => {
if (onSuccess) onSuccess();
navigate(Routes.REFERRALS);
}, RELOAD_DELAY);
}
}, [navigate, status]);
}, [navigate, onSuccess, status]);
// show "code applied" message when successfully applied
if (status === 'successful') {
@@ -207,6 +261,18 @@ export const ApplyCodeForm = () => {
};
}
if (status === 'no-funds') {
return {
disabled: false,
children: t('Deposit funds'),
type: 'button' as ButtonHTMLAttributes<HTMLButtonElement>['type'],
onClick: ((event) => {
event.preventDefault();
setViews({ type: ViewType.Deposit }, currentRouteId);
}) as MouseEventHandler,
};
}
if (status === 'requested') {
return {
disabled: true,
@@ -236,7 +302,9 @@ export const ApplyCodeForm = () => {
{t('Apply a referral code')}
</h3>
<p className="mb-4 text-center text-base">
{t('Enter a referral code to get trading discounts.')}
{t(
'Apply a referral code to access the discount benefits of the current program.'
)}
</p>
<form
className={classNames('flex w-full flex-col gap-4', {
@@ -251,8 +319,10 @@ export const ApplyCodeForm = () => {
{...register('code', {
required: t('You have to provide a code to apply it.'),
validate: (value) => {
const err = validateCode(value, t);
if (err !== true) return err;
const codeErr = validateCode(value, t);
if (codeErr !== true) return codeErr;
const fundsErr = validateFundsAvailable();
if (fundsErr !== true) return fundsErr;
return validateSet();
},
})}
@@ -262,10 +332,26 @@ export const ApplyCodeForm = () => {
</label>
<RainbowButton variant="border" {...getButtonProps()} />
</form>
{errors.code && (
<InputError className="overflow-auto break-words">
{errors.code.message?.toString()}
{status === 'no-funds' ? (
<InputError intent="warning" className="overflow-auto break-words">
<span>
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
</span>
</InputError>
) : (
errors.code && (
<InputError intent="warning" className="overflow-auto break-words">
{errors.code.message === SPAM_PROTECTION_ERR ? (
<span>
<SpamProtectionErr
requiredFunds={requiredFunds?.toString()}
/>
</span>
) : (
errors.code.message?.toString()
)}
</InputError>
)
)}
</div>
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
@@ -6,6 +6,8 @@ export const SKY_BACKGROUND =
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local';
// TODO: Update the links to use the correct referral related pages
export const REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
export const ABOUT_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
export const REFERRAL_DOCS_LINK =
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
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/';
@@ -19,14 +19,22 @@ import {
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
import {
ABOUT_REFERRAL_DOCS_LINK,
DISCLAIMER_REFERRAL_DOCS_LINK,
} from './constants';
import { useReferral } from './hooks/use-referral';
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { useT } from '../../lib/use-t';
import { Navigate } from 'react-router-dom';
import { Routes } from '../../lib/links';
import { useReferralProgram } from './hooks/use-referral-program';
export const CreateCodeContainer = () => {
const { pubKey } = useVegaWallet();
const isInReferralSet = useIsInReferralSet(pubKey);
// Navigate to the index page when already in the referral set.
if (isInReferralSet) {
return <Navigate to={Routes.REFERRALS} />;
}
return <CreateCodeForm />;
};
@@ -48,7 +56,7 @@ export const CreateCodeForm = () => {
</h3>
<p className="mb-4 text-center text-base">
{t(
'Generate a referral code to share with your friends and start earning commission.'
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
)}
</p>
@@ -98,10 +106,7 @@ const CreateCodeDialog = ({
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
const { data: referralSets } = useReferral({
pubKey,
role: 'referrer',
});
const { details: programDetails } = useReferralProgram();
const onSubmit = () => {
if (isReadOnly || !pubKey) {
@@ -201,7 +206,7 @@ const CreateCodeDialog = ({
);
}
if (!referralSets) {
if (!programDetails) {
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
@@ -237,7 +242,9 @@ const CreateCodeDialog = ({
intent={Intent.Primary}
onClick={() => onSubmit()}
{...getButtonProps()}
></TradingButton>
>
{t('Yes')}
</TradingButton>
{status === 'idle' && (
<TradingButton
fill={true}
@@ -255,9 +262,6 @@ const CreateCodeDialog = ({
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
{t('About the referral program')}
</ExternalLink>
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
{t('Disclaimer')}
</ExternalLink>
</div>
</div>
);
@@ -268,7 +272,7 @@ const CreateCodeDialog = ({
{(status === 'idle' || status === 'loading' || status === 'error') && (
<p>
{t(
'Generate a referral code to share with your friends and start earning commission.'
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
)}
</p>
)}
@@ -299,9 +303,6 @@ const CreateCodeDialog = ({
<ExternalLink href={ABOUT_REFERRAL_DOCS_LINK}>
{t('About the referral program')}
</ExternalLink>
<ExternalLink href={DISCLAIMER_REFERRAL_DOCS_LINK}>
{t('Disclaimer')}
</ExternalLink>
</div>
</div>
);
@@ -53,7 +53,7 @@ export const NotFound = () => {
const navigate = useNavigate();
return (
<div className="pt-32">
<LayoutWithSky className="pt-32">
<div
aria-hidden
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
@@ -75,6 +75,6 @@ export const NotFound = () => {
{t('Go back and try again')}
</RainbowButton>
</p>
</div>
</LayoutWithSky>
);
};
@@ -0,0 +1,20 @@
query FundsAvailable($partyId: ID!) {
party(id: $partyId) {
accountsConnection {
edges {
node {
balance
asset {
decimals
symbol
id
}
}
}
}
}
networkParameter(key: "spam.protection.applyReferral.min.funds") {
key
value
}
}
@@ -0,0 +1,63 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type FundsAvailableQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type FundsAvailableQuery = { __typename?: 'Query', party?: { __typename?: 'Party', accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', balance: string, asset: { __typename?: 'Asset', decimals: number, symbol: string, id: string } } } | null> | null } | null } | null, networkParameter?: { __typename?: 'NetworkParameter', key: string, value: string } | null };
export const FundsAvailableDocument = gql`
query FundsAvailable($partyId: ID!) {
party(id: $partyId) {
accountsConnection {
edges {
node {
balance
asset {
decimals
symbol
id
}
}
}
}
}
networkParameter(key: "spam.protection.applyReferral.min.funds") {
key
value
}
}
`;
/**
* __useFundsAvailableQuery__
*
* To run a query within a React component, call `useFundsAvailableQuery` and pass it any options that fit your needs.
* When your component renders, `useFundsAvailableQuery` 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 } = useFundsAvailableQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useFundsAvailableQuery(baseOptions: Apollo.QueryHookOptions<FundsAvailableQuery, FundsAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FundsAvailableQuery, FundsAvailableQueryVariables>(FundsAvailableDocument, options);
}
export function useFundsAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FundsAvailableQuery, FundsAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FundsAvailableQuery, FundsAvailableQueryVariables>(FundsAvailableDocument, options);
}
export type FundsAvailableQueryHookResult = ReturnType<typeof useFundsAvailableQuery>;
export type FundsAvailableLazyQueryHookResult = ReturnType<typeof useFundsAvailableLazyQuery>;
export type FundsAvailableQueryResult = Apollo.QueryResult<FundsAvailableQuery, FundsAvailableQueryVariables>;
@@ -0,0 +1,48 @@
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useFundsAvailableQuery } from './__generated__/FundsAvailable';
import compact from 'lodash/compact';
import BigNumber from 'bignumber.js';
/**
* Gets the funds for given public key and required min for
* the referral program.
*
* (Uses currently connected public key if left empty)
*/
export const useFundsAvailable = (pubKey?: string) => {
const { pubKey: currentPubKey } = useVegaWallet();
const partyId = pubKey || currentPubKey;
const { data, stopPolling } = useFundsAvailableQuery({
variables: { partyId: partyId || '' },
skip: !partyId,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
pollInterval: 5000,
});
const fundsAvailable = data
? compact(data.party?.accountsConnection?.edges?.map((e) => e?.node))
: undefined;
const requiredFunds = data
? BigNumber(data.networkParameter?.value || '0')
: undefined;
const sumOfFunds =
fundsAvailable
?.filter((fa) => fa.balance)
.reduce((sum, fa) => sum.plus(BigNumber(fa.balance)), BigNumber(0)) ||
BigNumber(0);
if (requiredFunds && sumOfFunds.isGreaterThanOrEqualTo(requiredFunds)) {
stopPolling();
}
return {
fundsAvailable,
requiredFunds,
isEligible:
fundsAvailable != null &&
requiredFunds != null &&
sumOfFunds.isGreaterThanOrEqualTo(requiredFunds),
};
};
@@ -1,14 +1,8 @@
import { getNumberFormat } from '@vegaprotocol/utils';
import { addDays } from 'date-fns';
import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
const STAKING_TIERS_MAPPING: Record<number, string> = {
1: 'Tradestarter',
2: 'Mid level degen',
3: 'Reward hoarder',
};
import BigNumber from 'bignumber.js';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MOCK = {
@@ -16,46 +10,76 @@ const MOCK = {
currentReferralProgram: {
id: 'abc',
version: 1,
endOfProgramTimestamp: addDays(new Date(), 10).toISOString(),
windowLength: 10,
benefitTiers: [
{
minimumEpochs: 5,
minimumRunningNotionalTakerVolume: '30000',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
{
minimumEpochs: 5,
minimumRunningNotionalTakerVolume: '20000',
referralDiscountFactor: '0.05',
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '100000',
referralDiscountFactor: '0.1',
referralRewardFactor: '0.05',
},
{
minimumEpochs: 5,
minimumRunningNotionalTakerVolume: '10000',
referralDiscountFactor: '0.001',
referralRewardFactor: '0.001',
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '1000000',
referralDiscountFactor: '0.1',
referralRewardFactor: '0.075',
},
{
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '5000000',
referralDiscountFactor: '0.1',
referralRewardFactor: '0.1',
},
{
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '25000000',
referralDiscountFactor: '0.1',
referralRewardFactor: '0.125',
},
{
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '75000000',
referralDiscountFactor: '0.1',
referralRewardFactor: '0.15',
},
{
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '150000000',
referralDiscountFactor: '0.07',
referralRewardFactor: '0.175',
},
],
stakingTiers: [
{
minimumStakedTokens: '10000',
referralRewardMultiplier: '1',
minimumStakedTokens: '100000000000000000000',
referralRewardMultiplier: '1.025',
},
{
minimumStakedTokens: '20000',
referralRewardMultiplier: '2',
minimumStakedTokens: '1000000000000000000000',
referralRewardMultiplier: '1.05',
},
{
minimumStakedTokens: '30000',
referralRewardMultiplier: '3',
minimumStakedTokens: '5000000000000000000000',
referralRewardMultiplier: '1.1',
},
{
minimumStakedTokens: '50000000000000000000000',
referralRewardMultiplier: '1.2',
},
{
minimumStakedTokens: '250000000000000000000000',
referralRewardMultiplier: '1.25',
},
{
minimumStakedTokens: '500000000000000000000000',
referralRewardMultiplier: '1.3',
},
],
endOfProgramTimestamp: '2024-12-31T01:00:00Z',
windowLength: 30,
},
loading: false,
error: undefined,
},
loading: false,
error: undefined,
};
export const useReferralProgram = () => {
@@ -79,9 +103,9 @@ export const useReferralProgram = () => {
return {
tier: i + 1, // sorted in asc order, hence first is the lowest tier
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
commission: BigNumber(t.referralRewardFactor).times(100).toFixed(2) + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
@@ -90,13 +114,11 @@ export const useReferralProgram = () => {
};
});
const stakingTiers = sortBy(
data.currentReferralProgram.stakingTiers,
(t) => t.referralRewardMultiplier
const stakingTiers = sortBy(data.currentReferralProgram.stakingTiers, (t) =>
parseFloat(t.referralRewardMultiplier)
).map((t, i) => {
return {
tier: i + 1,
label: STAKING_TIERS_MAPPING[i + 1],
...t,
};
});
@@ -75,10 +75,7 @@ export const useReferralToasts = () => {
data-testid="toast-apply-code"
size="xs"
onClick={() => {
const matched = matchPath(
Routes.REFERRALS_APPLY_CODE,
pathname
);
const matched = matchPath(Routes.REFERRALS, pathname);
if (!matched) navigate(Routes.REFERRALS_APPLY_CODE);
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
@@ -2,7 +2,10 @@ import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useCallback } from 'react';
import { useRefereesQuery } from './__generated__/Referees';
import compact from 'lodash/compact';
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
import type {
ReferralSetsQuery,
ReferralSetsQueryVariables,
} from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available';
@@ -118,3 +121,36 @@ export const useReferral = (args: UseReferralArgs) => {
refetch,
};
};
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
data?.referralSets.edges && data.referralSets.edges.length > 0
? data.referralSets.edges[0]?.node
: undefined;
export const useIsInReferralSet = (pubKey: string | null) => {
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)
);
};
@@ -13,7 +13,6 @@ export const useStakeAvailable = (pubKey?: string) => {
const { data } = useStakeAvailableQuery({
variables: { partyId: partyId || '' },
skip: !partyId,
// TODO: remove when network params available
errorPolicy: 'ignore',
});
@@ -15,11 +15,16 @@ export const LandingBanner = () => {
</div>
<div className="pt-20 sm:w-[50%]">
<h1 className="text-6xl font-alpha calt mb-10">
{t('Earn commission & stake rewards')}
{t('Vega community referrals')}
</h1>
<p className="text-lg mb-1">
{t(
'Referral programs can be proposed and created via community governance.'
)}
</p>
<p className="text-lg mb-10">
{t(
'Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.'
'Once live, users can generate referral codes to share with their friends and earn commission on their trades, while referred traders can access fee discounts based on the running volume of the group.'
)}
</p>
</div>
@@ -0,0 +1,28 @@
import { DocsLinks } from '@vegaprotocol/environment';
import { ExternalLink, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
export const QUSDTooltip = () => {
const t = useT();
return (
<Tooltip
description={
<>
<p className="mb-1">
{t(
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
)}
</p>
{DocsLinks && (
<ExternalLink href={DocsLinks.QUANTUM}>
{t('Find out more')}
</ExternalLink>
)}
</>
}
underline={true}
>
<span>{t('qUSD')}</span>
</Tooltip>
);
};
@@ -275,30 +275,34 @@ jest.mock('@vegaprotocol/wallet', () => {
});
describe('ReferralStatistics', () => {
it('displays create code when no data has been found for given pubkey', () => {
it('displays apply code when no data has been found for given pubkey', () => {
const { queryByTestId } = render(
<MockedProvider mocks={[]} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
<MemoryRouter>
<MockedProvider mocks={[]} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
expect(queryByTestId('referral-create-code-form')).toBeInTheDocument();
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
});
it('displays referrer stats when given pubkey is a referrer', async () => {
const { queryByTestId } = render(
<MockedProvider
mocks={[
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
@@ -4,13 +4,10 @@ import {
VegaIcon,
VegaIconNames,
truncateMiddle,
ExternalLink,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
import { CreateCodeContainer } from './create-code-form';
import classNames from 'classnames';
import { Table } from './table';
import {
@@ -26,34 +23,39 @@ import compact from 'lodash/compact';
import { useReferralProgram } from './hooks/use-referral-program';
import { useStakeAvailable } from './hooks/use-stake-available';
import sortBy from 'lodash/sortBy';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import { DocsLinks } from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
import { ApplyCodeForm } from './apply-code-form';
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
import { QUSDTooltip } from './qusd-tooltip';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
const program = useReferralProgram();
const { data: referee } = useReferral({
const { data: referee, refetch: refereeRefetch } = useReferral({
pubKey,
role: 'referee',
aggregationEpochs: program.details?.windowLength,
});
const { data: referrer } = useReferral({
const { data: referrer, refetch: referrerRefetch } = useReferral({
pubKey,
role: 'referrer',
aggregationEpochs: program.details?.windowLength,
});
const refetch = useCallback(() => {
refereeRefetch();
referrerRefetch();
}, [refereeRefetch, referrerRefetch]);
if (referee?.code) {
return (
<>
<Statistics data={referee} program={program} as="referee" />;
<Statistics data={referee} program={program} as="referee" />
{!referee.isEligible && <ApplyCodeForm />}
</>
);
@@ -62,13 +64,13 @@ export const ReferralStatistics = () => {
if (referrer?.code) {
return (
<>
<Statistics data={referrer} program={program} as="referrer" />;
<Statistics data={referrer} program={program} as="referrer" />
<RefereesTable data={referrer} program={program} />
</>
);
}
return <CreateCodeContainer />;
return <ApplyCodeFormContainer onSuccess={refetch} />;
};
export const useStats = ({
@@ -81,7 +83,9 @@ export const useStats = ({
as?: 'referrer' | 'referee';
}) => {
const { benefitTiers } = program;
const { data: epochData } = useCurrentEpochInfoQuery();
const { data: epochData } = useCurrentEpochInfoQuery({
fetchPolicy: 'network-only',
});
const { data: statsData } = useReferralSetStatsQuery({
variables: {
code: data?.code || '',
@@ -115,7 +119,7 @@ export const useStats = ({
: 1;
const finalCommissionValue = isNaN(multiplier)
? baseCommissionValue
: multiplier * baseCommissionValue;
: new BigNumber(multiplier).times(baseCommissionValue).toNumber();
const discountFactorValue = refereeStats?.discountFactor
? Number(refereeStats.discountFactor)
@@ -174,6 +178,7 @@ export const Statistics = ({
discountFactorValue,
currentBenefitTierValue,
epochsValue,
nextBenefitTierValue,
nextBenefitTierVolumeValue,
nextBenefitTierEpochsValue,
} = useStats({ data, program, as });
@@ -207,6 +212,7 @@ export const Statistics = ({
).toString(),
}
)}
overrideWithNoProgram={!details}
>
{baseCommissionValue * 100}%
</StatTile>
@@ -229,22 +235,28 @@ export const Statistics = ({
})}
</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)
? `(${baseCommissionValue * 100}% ⨉ ${multiplier} = ${
finalCommissionValue * 100
}%)`
? `(${baseCommissionFormatted}% ⨉ ${multiplier} = ${finalCommissionFormatted}%)`
: undefined
}
overrideWithNoProgram={!details}
>
{finalCommissionValue * 100}%
{finalCommissionFormatted}%
</StatTile>
);
const numberOfTradersValue = data.referees.length;
@@ -264,6 +276,7 @@ export const Statistics = ({
title={t('myVolume', 'My volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
overrideWithNoProgram={!details}
>
{compactNumFormat.format(referrerVolumeValue)}
</StatTile>
@@ -274,7 +287,7 @@ export const Statistics = ({
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile
title={t('totalCommission', 'Total commission (last {{count}}} epochs)', {
title={t('totalCommission', 'Total commission (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
description={<QUSDTooltip />}
@@ -301,15 +314,25 @@ export const Statistics = ({
);
const currentBenefitTierTile = (
<StatTile title={t('Current tier')}>
<StatTile
title={t('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')}>
{isApplyCodePreview
<StatTile title={t('Discount')} overrideWithNoProgram={!details}>
{isApplyCodePreview && benefitTiers.length >= 1
? benefitTiers[0].discountFactor * 100
: discountFactorValue * 100}
%
@@ -324,6 +347,7 @@ export const Statistics = ({
count: details?.windowLength,
}
)}
overrideWithNoProgram={!details}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
@@ -332,14 +356,14 @@ export const Statistics = ({
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
);
const nextTierVolumeTile = (
<StatTile title={t('Volume to next tier')}>
<StatTile title={t('Volume to next tier')} overrideWithNoProgram={!details}>
{nextBenefitTierVolumeValue <= 0
? '0'
: compactNumFormat.format(nextBenefitTierVolumeValue)}
</StatTile>
);
const nextTierEpochsTile = (
<StatTile title={t('Epochs to next tier')}>
<StatTile title={t('Epochs to next tier')} overrideWithNoProgram={!details}>
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
</StatTile>
);
@@ -461,6 +485,7 @@ export const RefereesTable = ({
count:
details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}}
components={[<QUSDTooltip key="qusd" />]}
ns={ns}
/>
),
@@ -492,28 +517,3 @@ export const RefereesTable = ({
</>
);
};
export const QUSDTooltip = () => {
const t = useT();
return (
<Tooltip
description={
<>
<p className="mb-1">
{t(
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
)}
</p>
{DocsLinks && (
<ExternalLink href={DocsLinks.QUANTUM}>
{t('Find out more')}
</ExternalLink>
)}
</>
}
underline={true}
>
<span>{t('qUSD')}</span>
</Tooltip>
);
};
@@ -4,11 +4,10 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { HowItWorksTable } from './how-it-works-table';
import { LandingBanner } from './landing-banner';
import { TiersContainer } from './tiers';
import { TabLink } from './buttons';
import { Outlet } from 'react-router-dom';
import { Outlet, useMatch } from 'react-router-dom';
import { Routes } from '../../lib/links';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
@@ -22,12 +21,13 @@ import { ErrorBoundary } from '../../components/error-boundary';
const Nav = () => {
const t = useT();
const match = useMatch(Routes.REFERRALS_APPLY_CODE);
return (
<div className="flex justify-center border-b border-vega-cdark-500">
<TabLink end to={Routes.REFERRALS}>
{t('I want a code')}
<TabLink end to={match ? Routes.REFERRALS_APPLY_CODE : Routes.REFERRALS}>
{t('Apply code')}
</TabLink>
<TabLink to={Routes.REFERRALS_APPLY_CODE}>{t('I have a code')}</TabLink>
<TabLink to={Routes.REFERRALS_CREATE_CODE}>{t('Create code')}</TabLink>
</div>
);
};
@@ -96,15 +96,13 @@ export const Referrals = () => {
<h2 className="text-2xl">{t('How it works')}</h2>
</div>
<div className="md:w-[60%] mx-auto">
<HowItWorksTable />
<div className="mt-5">
<TradingAnchorButton
className="mx-auto w-max"
href={REFERRAL_DOCS_LINK}
target="_blank"
>
{t('Read the terms')}{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
{t('Read the docs')} <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</TradingAnchorButton>
</div>
</div>
+4 -2
View File
@@ -14,8 +14,10 @@ export const Tag = ({
className={classNames(
'w-max border rounded-[1rem] py-[0.125rem] px-2 text-xs',
{
'border-vega-yellow-500 text-vega-yellow-500': color === 'yellow',
'border-vega-green-500 text-vega-green-500': color === 'green',
'border-vega-yellow-550 text-vega-yellow-550 dark:border-vega-yellow-500 dark:text-vega-yellow-500':
color === 'yellow',
'border-vega-green-550 text-vega-green-550 dark:border-vega-green-500 dark:text-vega-green-500':
color === 'green',
'border-vega-blue-500 text-vega-blue-500': color === 'blue',
'border-vega-purple-500 text-vega-purple-500': color === 'purple',
'border-vega-pink-500 text-vega-pink-500': color === 'pink',
+196 -88
View File
@@ -1,20 +1,43 @@
import { getDateTimeFormat } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { useReferralProgram } from './hooks/use-referral-program';
import { Table } from './table';
import classNames from 'classnames';
import { BORDER_COLOR, GRADIENT } from './constants';
import { Tag } from './tag';
import type { ComponentProps, ReactNode } from 'react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import {
DApp,
DocsLinks,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
// rainbow-ish order
const TIER_COLORS: Array<ComponentProps<typeof Tag>['color']> = [
'pink',
'orange',
'yellow',
'green',
'blue',
'purple',
];
const getTierColor = (tier: number) => {
const tiers = Object.keys(TIER_COLORS).length;
let index = Math.abs(tier - 1);
if (tier >= tiers) {
index = index % tiers;
}
return TIER_COLORS[index];
};
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
<div
className={classNames(
@@ -28,51 +51,63 @@ const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
const StakingTier = ({
tier,
label,
referralRewardMultiplier,
minimumStakedTokens,
}: {
tier: number;
label: string;
referralRewardMultiplier: string;
minimumStakedTokens: string;
}) => {
const t = useT();
const color: Record<number, ComponentProps<typeof Tag>['color']> = {
1: 'green',
2: 'blue',
3: 'pink',
};
const minimum = addDecimalsFormatNumber(minimumStakedTokens, 18);
// TODO: Decide what to do with the multiplier images
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const multiplierImage = (
<div
aria-hidden
className={classNames(
'w-full max-w-[80px] h-full min-h-[80px]',
'bg-cover bg-right-bottom',
{
"bg-[url('/1x.png')]": tier === 1,
"bg-[url('/2x.png')]": tier === 2,
"bg-[url('/3x.png')]": tier === 3,
}
)}
>
<span className="sr-only">{`${referralRewardMultiplier}x multiplier`}</span>
</div>
);
return (
<div
className={classNames(
'overflow-hidden',
'border rounded-md w-full',
'flex flex-row',
'bg-white dark:bg-vega-cdark-900',
GRADIENT,
BORDER_COLOR
)}
>
<div aria-hidden className="max-w-[120px]">
{tier < 4 && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={`/${tier}x.png`}
alt={`${referralRewardMultiplier}x multiplier`}
width={240}
height={240}
className="w-full h-full"
/>
<div
className={classNames(
'p-3 flex flex-row min-h-[80px] h-full items-center'
)}
</div>
<div className={classNames('p-3')}>
<Tag color={color[tier]}>Multiplier {referralRewardMultiplier}x</Tag>
<h3 className="mt-1 mb-1 text-base">{label}</h3>
<p className="text-sm text-vega-clight-100 dark:text-vega-cdark-100">
{t('Stake a minimum of {{minimumStakedTokens}} $VEGA tokens', {
minimumStakedTokens,
})}
</p>
>
<div>
<Tag color={getTierColor(tier)}>
{t('Multiplier')} {referralRewardMultiplier}x
</Tag>
<p className="mt-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
<Trans
defaults="Stake a minimum of <0>{{minimum}}</0> $VEGA tokens"
values={{ minimum }}
components={[<b key={minimum}></b>]}
/>
</p>
</div>
</div>
</div>
);
@@ -91,21 +126,29 @@ export const TiersContainer = () => {
if ((!loading && !details) || error) {
return (
<div className="text-base px-5 py-10 text-center">
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20 text-sm text-center">
<Trans
defaults="There are currently no active referral programs. Check the <0>Governance App</0> to see if there are any proposals in progress and vote."
components={[
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)} key="link">
<ExternalLink
href={governanceLink(TOKEN_PROPOSALS)}
key="link"
className="underline"
>
{t('Governance App')}
</ExternalLink>,
]}
ns={ns}
/>
/>{' '}
<Trans
defaults="You can propose a new program via the <0>Docs</0>."
defaults="Use the <0>docs</0> tutorial to propose a new program."
components={[
<ExternalLink href={DocsLinks?.REFERRALS} key="link">
{t('Docs')}
<ExternalLink
href={DocsLinks?.REFERRALS}
key="link"
className="underline"
>
{t('docs')}
</ExternalLink>,
]}
ns={ns}
@@ -116,47 +159,93 @@ export const TiersContainer = () => {
return (
<>
{/* Benefit tiers */}
<div className="flex flex-col items-baseline justify-between mt-10 mb-5">
<h2 className="text-2xl">{t('Referral tiers')}</h2>
<h2 className="text-3xl mt-10">{t('Current program details')}</h2>
{details?.id && (
<p>
<Trans
defaults="As a result of governance proposal <0>{{proposal}}</0> the program below is currently active on the Vega network."
values={{ proposal: truncateMiddle(details.id) }}
components={[
<ExternalLink
key="referral-program-proposal-link"
href={governanceLink(TOKEN_PROPOSAL.replace(':id', details.id))}
className="underline"
>
proposal
</ExternalLink>,
]}
/>
</p>
)}
{/* Meta */}
<div className="mt-10 flex flex-row items-baseline justify-between text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-alpha calt">
{details?.id && (
<span>
{t('Proposal ID:')}{' '}
<ExternalLink
href={governanceLink(TOKEN_PROPOSAL.replace(':id', details.id))}
>
<span>{truncateMiddle(details.id)}</span>
</ExternalLink>
</span>
)}
{ends && (
<span className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
<span>
{t('Program ends:')} {ends}
</span>
)}
</div>
<div className="mb-20">
{loading || !benefitTiers || benefitTiers.length === 0 ? (
<Loading variant="large" />
) : (
<TiersTable
windowLength={details?.windowLength}
data={benefitTiers.map((bt) => ({
...bt,
tierElement: (
<div className="rounded-full bg-vega-clight-900 dark:bg-vega-cdark-900 p-1 w-8 h-8 text-center">
{bt.tier}
</div>
),
}))}
/>
)}
</div>
{/* Staking tiers */}
<div className="flex flex-row items-baseline justify-between mb-5">
<h2 className="text-2xl">{t('Staking multipliers')}</h2>
</div>
<div className="mb-20 flex flex-col justify-items-stretch lg:flex-row gap-5">
{loading || !stakingTiers || stakingTiers.length === 0 ? (
<>
{/* Container */}
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20">
{/* Benefit tiers */}
<div className="flex flex-col mb-5">
<h3 className="text-2xl calt">{t('Benefit tiers')}</h3>
<p className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
{t(
'Members of a referral group can access the increasing commission and discount benefits defined in the program based on their combined running volume.'
)}
</p>
</div>
<div className="mb-10">
{loading || !benefitTiers || benefitTiers.length === 0 ? (
<Loading variant="large" />
<Loading variant="large" />
<Loading variant="large" />
</>
) : (
<StakingTiers data={stakingTiers} />
)}
) : (
<TiersTable
windowLength={details?.windowLength}
data={benefitTiers.map((bt) => ({
...bt,
tierElement: (
<div className="rounded-full bg-vega-clight-900 dark:bg-vega-cdark-900 p-1 w-8 h-8 text-center">
{bt.tier}
</div>
),
}))}
/>
)}
</div>
{/* Staking tiers */}
<div className="flex flex-col mb-5">
<h3 className="text-2xl calt">{t('Staking multipliers')}</h3>
<p className="text-sm text-vega-clight-200 dark:text-vega-cdark-200">
{t(
'Referrers can access the commission multipliers defined in the program by staking VEGA tokens in the amounts shown.'
)}
</p>
</div>
<div className="gap-5 grid lg:grid-cols-3">
{loading || !stakingTiers || stakingTiers.length === 0 ? (
<>
<Loading variant="large" />
<Loading variant="large" />
<Loading variant="large" />
</>
) : (
<StakingTiers data={stakingTiers} />
)}
</div>
</div>
</>
);
@@ -168,17 +257,14 @@ const StakingTiers = ({
data: ReturnType<typeof useReferralProgram>['stakingTiers'];
}) => (
<>
{data.map(
({ tier, label, referralRewardMultiplier, minimumStakedTokens }, i) => (
<StakingTier
key={i}
tier={tier}
label={label}
referralRewardMultiplier={referralRewardMultiplier}
minimumStakedTokens={minimumStakedTokens}
/>
)
)}
{data.map(({ tier, referralRewardMultiplier, minimumStakedTokens }, i) => (
<StakingTier
key={i}
tier={tier}
referralRewardMultiplier={referralRewardMultiplier}
minimumStakedTokens={minimumStakedTokens}
/>
))}
</>
);
@@ -203,9 +289,17 @@ const TiersTable = ({
{
name: 'commission',
displayName: t('Referrer commission'),
tooltip: t('A percentage of commission earned by the referrer'),
tooltip: t(
"The proportion of the referee's taker fees to be rewarded to the referrer"
),
},
{
name: 'discount',
displayName: t('Referee trading discount'),
tooltip: t(
"The proportion of the referee's taker fees to be discounted"
),
},
{ name: 'discount', displayName: t('Referrer trading discount') },
{
name: 'volume',
displayName: t(
@@ -215,20 +309,34 @@ const TiersTable = ({
count: windowLength,
}
),
tooltip: t('The minimum running notional for the given benefit tier'),
},
{
name: 'epochs',
displayName: t('Min. epochs'),
tooltip: t(
'The minimum number of epochs the party needs to be in the referral set to be eligible for the benefit'
),
},
{ name: 'epochs', displayName: t('Min. epochs') },
]}
className="bg-white dark:bg-vega-cdark-900"
data={data.map((d) => ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier >= 3,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-yellow-400 dark:from-vega-yellow-600 to-20% bg-highlight':
'yellow' === getTierColor(d.tier),
'from-vega-green-400 dark:from-vega-green-600 to-20% bg-highlight':
'green' === getTierColor(d.tier),
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 1,
'blue' === getTierColor(d.tier),
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
'purple' === getTierColor(d.tier),
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
'pink' === getTierColor(d.tier),
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier == 0,
'orange' === getTierColor(d.tier),
'from-vega-clight-200 dark:from-vega-cdark-200 to-20% bg-highlight':
'none' === getTierColor(d.tier),
}),
}))}
/>
+24 -1
View File
@@ -34,8 +34,17 @@ type StatTileProps = {
title: string;
description?: ReactNode;
children?: ReactNode;
overrideWithNoProgram?: boolean;
};
export const StatTile = ({ title, description, children }: StatTileProps) => {
export const StatTile = ({
title,
description,
children,
overrideWithNoProgram = false,
}: StatTileProps) => {
if (overrideWithNoProgram) {
return <NoProgramTile title={title} />;
}
return (
<Tile>
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
@@ -51,6 +60,20 @@ export const StatTile = ({ title, description, children }: StatTileProps) => {
);
};
export const NoProgramTile = ({ title }: Pick<StatTileProps, 'title'>) => {
const t = useT();
return (
<Tile title={title}>
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
{title}
</h3>
<div className="text-xs text-vega-clight-300 dark:text-vega-cdark-300 leading-[3rem]">
{t('No active program')}
</div>
</Tile>
);
};
const FADE_OUT_STYLE = classNames(
'after:w-5 after:h-full after:absolute after:top-0 after:right-0',
'after:bg-gradient-to-l after:from-vega-clight-800 after:dark:from-vega-cdark-800 after:to-transparent'
@@ -36,26 +36,6 @@ query RewardsPage($partyId: ID!) {
}
}
query ActivityStreak($partyId: ID!) {
partiesConnection(id: $partyId) {
edges {
node {
id
activityStreak {
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
}
}
}
}
}
query RewardsHistory(
$partyId: ID!
$epochRewardSummariesPagination: Pagination
@@ -10,13 +10,6 @@ export type RewardsPageQueryVariables = Types.Exact<{
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string } | null, activityStreak?: { __typename?: 'PartyActivityStreak', rewardVestingMultiplier: string, rewardDistributionMultiplier: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
export type ActivityStreakQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type ActivityStreakQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, activityStreak?: { __typename?: 'PartyActivityStreak', activeFor: number, isActive: boolean, inactiveFor: number, rewardDistributionMultiplier: string, rewardVestingMultiplier: string, epoch: number, tradedVolume: string, openVolume: string } | null } }> } | null };
export type RewardsHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>;
@@ -98,55 +91,6 @@ export function useRewardsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOption
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
export const ActivityStreakDocument = gql`
query ActivityStreak($partyId: ID!) {
partiesConnection(id: $partyId) {
edges {
node {
id
activityStreak {
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
}
}
}
}
}
`;
/**
* __useActivityStreakQuery__
*
* To run a query within a React component, call `useActivityStreakQuery` and pass it any options that fit your needs.
* When your component renders, `useActivityStreakQuery` 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 } = useActivityStreakQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useActivityStreakQuery(baseOptions: Apollo.QueryHookOptions<ActivityStreakQuery, ActivityStreakQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ActivityStreakQuery, ActivityStreakQueryVariables>(ActivityStreakDocument, options);
}
export function useActivityStreakLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ActivityStreakQuery, ActivityStreakQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ActivityStreakQuery, ActivityStreakQueryVariables>(ActivityStreakDocument, options);
}
export type ActivityStreakQueryHookResult = ReturnType<typeof useActivityStreakQuery>;
export type ActivityStreakLazyQueryHookResult = ReturnType<typeof useActivityStreakLazyQuery>;
export type ActivityStreakQueryResult = Apollo.QueryResult<ActivityStreakQuery, ActivityStreakQueryVariables>;
export const RewardsHistoryDocument = gql`
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
epochRewardSummaries(
@@ -1,347 +0,0 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useRewardsHistoryQuery } from './__generated__/Rewards';
import { useReferralProgram } from '../../client-pages/referrals/hooks/use-referral-program';
import { useState, useEffect } from 'react';
import { useT } from '../../lib/use-t';
import { useRewardsRowData } from './use-reward-row-data';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import classNames from 'classnames';
import {
Icon,
type IconName,
Intent,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
export const ActiveRewards = ({
epoch,
pubKey,
assets,
}: {
pubKey: string | null;
epoch: number;
assets: Record<string, AssetFieldsFragment>;
}) => {
const [epochVariables] = useState(() => ({
from: epoch - 1,
to: epoch,
}));
// No need to specify the fromEpoch as it will by default give you the last
const { refetch, data } = useRewardsHistoryQuery({
variables: {
partyId: pubKey || '',
fromEpoch: epochVariables.from,
toEpoch: epochVariables.to,
},
});
useEffect(() => {
const interval = setInterval(refetch, 30000);
return () => clearInterval(interval);
}, [refetch]);
const rowData = useRewardsRowData({
epochRewardSummaries: data?.epochRewardSummaries,
partyRewards: data?.party?.rewardsConnection,
assets,
partyId: pubKey,
});
const t = useT();
if (!pubKey) {
return (
<div className="pt-4">
<p className="text-muted text-sm">{t('Not connected')}</p>
</div>
);
}
// TODO: extract card component - call it reward tiles
return (
<div className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(343px,_1fr))]">
{rowData.map((row, i) => {
// TODO: filter out 0 values
// const entries = Object.entries(row).filter(([key, value]) => {
// value !== 0;
// });
return (
<div key={i}>
<div
className={classNames(
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
'rounded-lg',
'from-vega-blue-500 to-vega-green-400'
)}
>
<div className="bg-gradient-to-b from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4 flex flex-col gap-4">
<div className="flex justify-between gap-4">
<div className="flex flex-col gap-2 items-center text-center">
<span className="flex items-center p-2 rounded-full border border-gray-600">
<VegaIcon name={VegaIconNames.MAN} size={18} />
</span>
<span className="text-muted text-xs">
{t('Individual')}
</span>
</div>
<div className="flex flex-col gap-2 items-center text-center">
<span className="flex flex-col gap-1 font-alpha calt text-2xl shrink-1 text-center">
<span>
{`${addDecimalsFormatNumber(
row.total,
row.asset?.decimals || 0
)}`}
</span>
<span>{`${row.asset?.symbol}`}</span>
</span>
<Tooltip description={'pro rata'} underline={true}>
<span className="text-xs">{t('Pro rata')}</span>
</Tooltip>
</div>
<div className="flex flex-col gap-2 items-center text-center">
<span className="flex items-center p-2 rounded-full border border-gray-600">
<VegaIcon name={VegaIconNames.LOCK} size={18} />
</span>
<span className="text-muted text-xs whitespace-nowrap">
{t('11 epochs')}
</span>
</div>
</div>
<span className="border-[0.5px] border-gray-700" />
<span>
{t('Price taking')} {row.asset?.symbol} {row.asset?.name}
</span>
<div className="flex items-center gap-8 flex-wrap">
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')}</span>
<span>{t('5 epochs')}</span>
</span>
<span className="flex flex-col">
<span className="text-muted text-xs">
{t('Assessed over')}
</span>
<span>{t('5 epochs')}</span>
</span>
</div>
<span className="text-muted text-sm">
{t(
'Get rewards for taking prices on the order book and paying fees.'
)}
</span>
<span className="border-[0.5px] border-gray-700" />
{/* <div className="grid grid-cols-3 items-center gap-3"> */}
<div className="flex justify-between flex-wrap items-center gap-3">
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted text-xs">
{t('Entity scope')}{' '}
</span>
<span className="flex items-center gap-1">
<span className="flex items-center p-1 rounded-full border border-gray-600">
<VegaIcon name={VegaIconNames.MAN} size={16} />
</span>
<StatusIndicator
intent={Intent.Success}
icon={IconNames.TICK_CIRCLE}
/>
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted text-xs">
{t('Amount staked')}{' '}
</span>
<span className="flex items-center gap-1">
{t('200 VEGA')}
<StatusIndicator
intent={Intent.Success}
icon={IconNames.TICK_CIRCLE}
/>
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted text-xs">
{t('Average position')}{' '}
</span>
<span className="flex items-center gap-1">
{t('100 USDT')}
<StatusIndicator
intent={Intent.Success}
icon={IconNames.TICK_CIRCLE}
/>
</span>
</span>
</div>
</div>
</div>
</div>
);
})}
</div>
);
};
// const getIconIntent = (status: string) => {
// switch (status) {
// case 'GOOD':
// return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
// case 'RETIRED':
// return { icon: IconNames.MOON, intent: Intent.None };
// case 'UNKNOWN':
// return { icon: IconNames.HELP, intent: Intent.Primary };
// case 'MALICIOUS':
// return { icon: IconNames.ERROR, intent: Intent.Danger };
// case 'SUSPICIOUS':
// return { icon: IconNames.ERROR, intent: Intent.Danger };
// case 'COMPROMISED':
// return { icon: IconNames.ERROR, intent: Intent.Danger };
// default:
// return { icon: IconNames.HELP, intent: Intent.Primary };
// }
// };
const StatusIndicator = ({
intent,
icon,
}: {
intent: Intent;
icon: string;
}) => {
return (
<span
className={classNames(
{
'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary,
'text-vega-green dark:text-vega-green': intent === Intent.Success,
'dark:text-yellow text-yellow-600': intent === Intent.Warning,
'text-vega-red': intent === Intent.Danger,
},
'flex items-start p-1 align-text-bottom'
)}
>
<Icon size={3} name={icon as IconName} />
</span>
);
};
export const ActivityStreak = ({
epoch,
pubKey,
assets,
}: {
pubKey: string | null;
epoch: number;
assets: Record<string, AssetFieldsFragment>;
}) => {
// const { data } = useActivityStreakQuery({
// variables: {
// partyId: pubKey || '',
// },
// });
const { benefitTiers } = useReferralProgram();
// const streaks = data?.partiesConnection?.edges?.map(
// (edge) => edge?.node?.activityStreak
// );
// @Input()
const progress = 30;
const total = 100;
const safeProgress = () => {
return (progress / total) * 100;
};
const progressBarHeight = 'h-6';
return (
<>
<div className="flex flex-col gap-1 w-full">
<div className="flex flex-col gap-1">
<div
className="grid"
style={{
gridTemplateColumns:
'repeat(' + benefitTiers.length + ', minmax(0, 1fr))',
}}
>
{benefitTiers.map((tier, index) => {
return (
<div key={index} className="flex justify-end -mr-10">
<span className="flex flex-col items-center gap-1">
<span className="flex flex-col items-center font-medium">
<span className="text-sm">Tier {tier.tier}</span>
<span className="text-muted text-xs">7 days</span>
</span>
<span className="text-xs flex flex-col items-center justify-center px-2 py-1 rounded-lg text-white border border-pink-600 bg-pink-900">
<span>Reward 1x</span>
<span>Vesting 1.5x</span>
</span>
<span className="text-pink-500 text-xl"></span>
</span>
</div>
);
})}
</div>
</div>
<div className="flex items-center gap-1">
{benefitTiers.map((tier, index) => {
return (
<div
key={index}
className="bg-white dark:bg-gray-800 shadow-card rounded-[100px] grow"
>
<div
className={classNames(
'relative w-full rounded-[100px] bg-gray-200 dark:bg-gray-800',
progressBarHeight
)}
>
<div
className="absolute left-0 top-0 h-full rounded-[100px] bg-gradient-to-r from-vega-pink-600 to-vega-pink-500"
style={{ width: safeProgress() + '%' }}
></div>
</div>
</div>
);
})}
</div>
<div className="flex items-center gap-1">
<VegaIcon name={VegaIconNames.STREAK} />
<span className="flex flex-col text-xs">
<span>4 days streak</span>
<span>
<span className="text-vega-pink-500">3 days</span> &nbsp;to Tier 1
</span>
</span>
</div>
</div>
{/* <div>{JSON.stringify(stakingTiers)}</div> */}
{/* <div>{JSON.stringify(details)}</div> */}
{/* <div>{JSON.stringify(benefitTiers)}</div> */}
{/* <div>{JSON.stringify(streaks)}</div> */}
</>
);
};
@@ -33,7 +33,6 @@ import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { RewardsHistoryContainer } from './rewards-history';
import { useT } from '../../lib/use-t';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { ActiveRewards, ActivityStreak } from './activity-rewards';
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
@@ -57,7 +56,6 @@ export const RewardsContainer = () => {
// No need to specify the fromEpoch as it will by default give you the last
// Note activityStreak in query will fail
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
variables: {
partyId: pubKey || '',
@@ -225,30 +223,6 @@ export const RewardsContainer = () => {
</Card>
);
})}
<Card
title={t('Activity streak')}
className="lg:col-span-full"
loading={rewardsLoading}
>
<span className="flex flex-col mx-8">
<ActivityStreak
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</span>
</Card>
<Card
title={t('Active rewards')}
className="lg:col-span-full"
loading={rewardsLoading}
>
<ActiveRewards
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</Card>
<Card
title={t('Rewards history')}
className="lg:col-span-full"
@@ -255,14 +255,6 @@ export const RewardHistoryTable = ({
return colDefs;
}, []);
if (!pubKey) {
return (
<div className="pt-4">
<p className="text-muted text-sm">{t('Not connected')}</p>
</div>
);
}
return (
<div>
<div className="mb-2 flex items-center justify-between gap-2">
@@ -350,7 +342,7 @@ export const RewardHistoryTable = ({
);
};
export const EpochInput = ({
const EpochInput = ({
id,
value,
max,
@@ -10,6 +10,7 @@ import { positionsDataProvider } from '@vegaprotocol/positions';
import { useGlobalStore } from '../../stores';
const ONBOARDING_STORAGE_KEY = 'vega_onboarding';
export const useOnboardingStore = create<{
dialogOpen: boolean;
walletDialogOpen: boolean;
@@ -20,7 +21,7 @@ export const useOnboardingStore = create<{
}>()(
persist(
(set) => ({
dialogOpen: true,
dialogOpen: false,
walletDialogOpen: false,
dismissed: false,
dismiss: () => set({ dismissed: true }),
@@ -1,18 +1,29 @@
import { useEffect } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { WelcomeDialogContent } from './welcome-dialog-content';
import { useOnboardingStore } from './use-get-onboarding-step';
import { VegaConnectDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../../lib/vega-connectors';
import { RiskMessage } from './risk-message';
import { useT } from '../../lib/use-t';
import { Routes } from '../../lib/links';
import { RiskMessage } from './risk-message';
import { WelcomeDialogContent } from './welcome-dialog-content';
import { useOnboardingStore } from './use-get-onboarding-step';
import { ensureSuffix } from '@vegaprotocol/utils';
/**
* A list of paths on which the welcome dialog should be omitted.
*/
const OMIT_ON_LIST = [ensureSuffix(Routes.REFERRALS, '/*')];
export const WelcomeDialog = () => {
const { pathname } = useLocation();
const t = useT();
const { VEGA_ENV } = useEnvironment();
const dismissed = useOnboardingStore((store) => store.dismissed);
const dialogOpen = useOnboardingStore((store) => store.dialogOpen);
const dismiss = useOnboardingStore((store) => store.dismiss);
const setDialogOpen = useOnboardingStore((store) => store.setDialogOpen);
const walletDialogOpen = useOnboardingStore(
(store) => store.walletDialogOpen
);
@@ -20,6 +31,16 @@ export const WelcomeDialog = () => {
(store) => store.setWalletDialogOpen
);
useEffect(() => {
const shouldOmit = OMIT_ON_LIST.map((path) =>
matchPath(path, pathname)
).some((m) => !!m);
if (dismissed || shouldOmit) return;
setDialogOpen(true);
}, [dismissed, pathname, setDialogOpen]);
const content = walletDialogOpen ? (
<VegaConnectDialog
connectors={Connectors}
@@ -31,7 +52,12 @@ export const WelcomeDialog = () => {
<WelcomeDialogContent />
);
const onClose = walletDialogOpen ? () => setWalletDialogOpen(false) : dismiss;
const onClose = walletDialogOpen
? () => setWalletDialogOpen(false)
: () => {
setDialogOpen(false);
dismiss();
};
const title = walletDialogOpen ? null : (
<span className="font-alpha calt" data-testid="welcome-title">
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.73.6
VEGA_VERSION=v0.73.8
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
VEGA_VERSION=v0.73.6
VEGA_VERSION=v0.73.8
+1 -1
View File
@@ -1,2 +1,2 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
VEGA_VERSION=v0.73.6
VEGA_VERSION=v0.73.8
+4 -4
View File
@@ -1159,9 +1159,9 @@ profile = ["pytest-profiling", "snakeviz"]
[package.source]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git"
reference = "HEAD"
resolved_reference = "fbcb974b2055bbc80169cdfd69987f087f9969fb"
url = "https://github.com/vegaprotocol/vega-market-sim.git/"
reference = "fix/genesis_panic"
resolved_reference = "7ab04931924380db8000544b7f3d65fcb39b5467"
[[package]]
name = "websocket-client"
@@ -1342,4 +1342,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.11"
content-hash = "d1231fe591b774e34b8f94a54cd02e4d7dae924c57785263841c3b0b0feed505"
content-hash = "68ed0de55290a3b929d47eb7f7b031fb7e172261c7bbeb4f554b7c27a4462754"
+2 -3
View File
@@ -31,7 +31,7 @@ initial_spread: float = 0.1
market_name = "BTC:DAI_2023"
@pytest.mark.usefixtures("vega", "page", "simple_market", "risk_accepted")
@pytest.mark.usefixtures("vega", "page", "simple_market", "risk_accepted", "auth")
def test_price_monitoring(simple_market, vega: VegaService, page: Page):
page.goto(f"/#/markets/all")
expect(page.locator(table_row_selector).locator(trading_mode_col)).to_have_text(
@@ -109,9 +109,8 @@ def test_price_monitoring(simple_market, vega: VegaService, page: Page):
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
).to_have_text("100.00 (>100%)")
vega.forward("10s")
vega.wait_fn(1)
vega.wait_fn(10)
vega.wait_for_total_catchup()
expect(
page.get_by_test_id(liquidity_supplied).get_by_test_id(item_value)
@@ -197,10 +197,10 @@ def test_market_info_risk_factors(page: Page):
fields = [
["Long", "0.05153"],
["Short", "0.05422"],
["Max Leverage Long", "19.036"],
["Max Leverage Short", "18.111"],
["Max Initial Leverage Long", "12.691"],
["Max Initial Leverage Short", "12.074"],
["Max Leverage Long", "19.406"],
["Max Leverage Short", "18.445"],
["Max Initial Leverage Long", "12.937"],
["Max Initial Leverage Short", "12.297"],
]
validate_info_section(page, fields)
@@ -137,6 +137,6 @@ def test_perps_market_terminated(page: Page, vega: VegaService):
expect(page.get_by_test_id("market-trading-mode")).to_have_text("Trading modeNo trading")
expect(page.get_by_test_id("market-state")).to_have_text("StatusClosed")
expect(page.get_by_test_id("liquidity-supplied")).to_have_text("Liquidity supplied 0.00 (0.00%)")
expect(page.get_by_test_id("market-funding")).to_have_text("Funding Rate / Countdown-Unknown")
expect(page.get_by_test_id("index-price")).to_have_text("Index Price-")
expect(page.get_by_test_id("market-funding")).to_contain_text("Funding Rate / Countdown")
expect(page.get_by_test_id("index-price")).to_contain_text("Index Price")
expect(page.get_by_test_id("deal-ticket-error-message-summary")).to_have_text("This market is closed and not accepting orders")
+4
View File
@@ -279,6 +279,7 @@
"This timestamp is user curated metadata and does not drive any on-chain functionality.": "This timestamp is user curated metadata and does not drive any on-chain functionality.",
"Tier": "Tier",
"to": "to",
"To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.": "To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.",
"Toast location": "Toast location",
"Total discount": "Total discount",
"Total distributed": "Total distributed",
@@ -296,6 +297,9 @@
"Trading on Market {{name}} may stop. There are open proposals to close this market": "Trading on Market {{name}} may stop. There are open proposals to close this market",
"Trading on Market {{name}} will stop on {{date}}": "Trading on Market {{name}} will stop on {{date}}",
"Transfer": "Transfer",
"totalCommission": "Total commission (last {{count}} epochs)",
"totalCommission_one": "Total commission (last {{count}} epoch)",
"totalCommission_other": "Total commission (last {{count}} epochs)",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"Vega Reward pot": "Vega Reward pot",
@@ -632,13 +632,9 @@ export const RiskFactorsInfoPanel = ({
const { short, long } = market.riskFactors;
const maxLeverageLong = new BigNumber(1).dividedBy(
new BigNumber(market.linearSlippageFactor).plus(long)
);
const maxLeverageLong = new BigNumber(1).dividedBy(long);
const maxLeverageShort = new BigNumber(1).dividedBy(
new BigNumber(market.linearSlippageFactor).plus(short)
);
const maxLeverageShort = new BigNumber(1).dividedBy(short);
const maxInitialLeverageLong = !market.tradableInstrument.marginCalculator
? undefined
+1 -1
View File
@@ -563,7 +563,7 @@ const WarningCell = ({
<div className="flex items-center justify-end">
{showIcon && (
<span className="mr-2 text-black dark:text-white">
<VegaIcon name={VegaIconNames.EXCLAMATION_MARK} size={12} />
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />
</span>
)}
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
@@ -94,7 +94,7 @@ export const ProtocolUpgradeCountdown = ({
}
)}
>
<VegaIcon name={VegaIconNames.EXCLAMATION_MARK} size={12} />{' '}
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />{' '}
<span className="flex flex-nowrap gap-1 whitespace-nowrap">
<span>{t('Network upgrade in {{countdown}}', { countdown })} </span>
</span>
@@ -1,4 +1,4 @@
export const IconExclamationMark = ({ size = 16 }: { size: number }) => {
export const IconExclaimationMark = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M8 0.879997L7.57 1.63L0.130005 14.5H15.87L8 0.879997ZM8.75 12H7.25V10.5H8.75V12ZM7.25 9.5V6H8.75V9.5H7.25Z" />
@@ -1,28 +0,0 @@
export const IconMan = ({ size = 14 }: { size: number }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 16 16"
className="stroke-current"
fill="none"
>
<path
d="M8.00016 7.99996C9.47292 7.99996 10.6668 6.80605 10.6668 5.33329C10.6668 3.86053 9.47292 2.66663 8.00016 2.66663C6.5274 2.66663 5.3335 3.86053 5.3335 5.33329C5.3335 6.80605 6.5274 7.99996 8.00016 7.99996Z"
stroke="#DCDEE3"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
className="stroke-current"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.00033 9.33333C5.79119 9.33333 4.00033 11.1242 4.00033 13.3333V13.8667C4.00033 13.9403 3.94063 14 3.86699 14H2.80033C2.72669 14 2.66699 13.9403 2.66699 13.8667V13.3333C2.66699 10.3878 5.05481 8 8.00033 8C10.9458 8 13.3337 10.3878 13.3337 13.3333V13.8667C13.3337 13.9403 13.274 14 13.2003 14H12.1337C12.06 14 12.0003 13.9403 12.0003 13.8667V13.3333C12.0003 11.1242 10.2095 9.33333 8.00033 9.33333Z"
fill="#DCDEE3"
className="stroke-current"
/>
</svg>
);
};
@@ -1,32 +0,0 @@
export const IconStreak = ({ size = 14 }: { size: number }) => {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="fillCurrent"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5.999 4H3.99902V5.99997H5.999V4Z" fill="fillCurrent" />
<path
d="M7.999 2.0001V0.00012207H5.99902V2.0001V4.00007H7.999V2.0001Z"
fill="fillCurrent"
/>
<path d="M3.99897 6H-0.000976562V7.99997H3.99897V6Z" fill="fillCurrent" />
<path
d="M5.999 7.99988H3.99902V9.99985H5.999V7.99988Z"
fill="fillCurrent"
/>
<path
d="M7.999 9.99994H5.99902V13.9999H7.999V9.99994Z"
fill="fillCurrent"
/>
<path
d="M9.999 7.99988H7.99902V9.99985H9.999V7.99988Z"
fill="fillCurrent"
/>
<path d="M13.999 6H9.99902V7.99997H13.999V6Z" fill="fillCurrent" />
<path d="M9.999 4H7.99902V5.99997H9.999V4Z" fill="fillCurrent" />
</svg>
);
};
@@ -1,19 +0,0 @@
export const IconTeam = ({ size = 14 }: { size: number }) => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Icon">
<path
id="Vector"
d="M13.1282 11.1863C12.7371 10.7949 12.2801 10.4754 11.7782 10.2426C12.486 9.66912 12.9375 8.79412 12.9375 7.81287C12.9375 6.08162 11.4938 4.6613 9.76254 4.68787C8.05785 4.71443 6.68441 6.10349 6.68441 7.81287C6.68441 8.79412 7.13754 9.66912 7.84379 10.2426C7.34177 10.4753 6.88476 10.7947 6.49379 11.1863C5.64066 12.041 5.15629 13.1691 5.12504 14.3722C5.12462 14.3889 5.12755 14.4055 5.13364 14.421C5.13974 14.4366 5.14888 14.4507 5.16053 14.4627C5.17218 14.4746 5.1861 14.4841 5.20147 14.4906C5.21684 14.497 5.23336 14.5004 5.25004 14.5004H6.12504C6.19223 14.5004 6.24848 14.4472 6.25004 14.3801C6.27973 13.4738 6.64691 12.6254 7.29223 11.9816C7.62245 11.6496 8.01523 11.3865 8.44784 11.2073C8.88045 11.0281 9.3443 10.9366 9.81254 10.9379C10.7641 10.9379 11.6594 11.3082 12.3329 11.9816C12.9766 12.6254 13.3438 13.4738 13.375 14.3801C13.3766 14.4472 13.4329 14.5004 13.5 14.5004H14.375C14.3917 14.5004 14.4082 14.497 14.4236 14.4906C14.439 14.4841 14.4529 14.4746 14.4646 14.4627C14.4762 14.4507 14.4853 14.4366 14.4914 14.421C14.4975 14.4055 14.5005 14.3889 14.5 14.3722C14.4688 13.1691 13.9844 12.041 13.1282 11.1863ZM9.81254 9.81287C9.27816 9.81287 8.77504 9.60505 8.39848 9.22693C8.2095 9.03944 8.06022 8.8158 7.95955 8.56937C7.85888 8.32293 7.80888 8.05874 7.81254 7.79255C7.81723 7.28005 8.02191 6.78474 8.37973 6.41755C8.75473 6.03318 9.25629 5.81912 9.79223 5.81287C10.3219 5.80818 10.836 6.01443 11.2141 6.38474C11.6016 6.76443 11.8141 7.27224 11.8141 7.81287C11.8141 8.34724 11.6063 8.8488 11.2282 9.22693C11.0427 9.41333 10.822 9.56109 10.579 9.66167C10.336 9.76224 10.0755 9.81363 9.81254 9.81287ZM5.89848 8.22537C5.88441 8.08943 5.8766 7.95193 5.8766 7.81287C5.8766 7.56443 5.90004 7.32224 5.94379 7.0863C5.95473 7.03005 5.92504 6.97224 5.87348 6.9488C5.66098 6.85349 5.46566 6.72224 5.29691 6.55662C5.09807 6.36382 4.9416 6.13168 4.83748 5.87503C4.73337 5.61837 4.6839 5.34283 4.69223 5.06599C4.70629 4.56443 4.90785 4.08787 5.25941 3.72849C5.64535 3.33318 6.1641 3.11755 6.71566 3.1238C7.2141 3.12849 7.69535 3.32068 8.05941 3.6613C8.18285 3.77693 8.2891 3.90505 8.37817 4.04255C8.40942 4.09099 8.47035 4.1113 8.52348 4.09255C8.79848 3.99724 9.0891 3.93005 9.38754 3.8988C9.47504 3.88943 9.52504 3.79568 9.48598 3.71755C8.97816 2.71287 7.94066 2.01912 6.74066 2.00037C5.00785 1.9738 3.5641 3.39412 3.5641 5.1238C3.5641 6.10505 4.01566 6.98005 4.72348 7.55349C4.2266 7.78318 3.76879 8.10037 3.37191 8.49724C2.51566 9.35193 2.03129 10.4801 2.00004 11.6847C1.99962 11.7014 2.00255 11.718 2.00864 11.7335C2.01474 11.7491 2.02388 11.7632 2.03553 11.7752C2.04718 11.7871 2.0611 11.7966 2.07647 11.8031C2.09184 11.8095 2.10836 11.8129 2.12504 11.8129H3.0016C3.06879 11.8129 3.12504 11.7597 3.1266 11.6926C3.15629 10.7863 3.52348 9.93787 4.16879 9.29412C4.62816 8.83474 5.19066 8.51599 5.80473 8.3613C5.86566 8.34568 5.90629 8.28787 5.89848 8.22537Z"
fill="white"
/>
</g>
</svg>
);
};
@@ -14,7 +14,7 @@ import { IconCopy } from './svg-icons/icon-copy';
import { IconCross } from './svg-icons/icon-cross';
import { IconDeposit } from './svg-icons/icon-deposit';
import { IconEdit } from './svg-icons/icon-edit';
import { IconExclamationMark } from './svg-icons/icon-exclamation-mark';
import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark';
import { IconEye } from './svg-icons/icon-eye';
import { IconEyeOff } from './svg-icons/icon-eye-off';
import { IconForum } from './svg-icons/icon-forum';
@@ -41,9 +41,6 @@ import { IconTwitter } from './svg-icons/icon-twitter';
import { IconVote } from './svg-icons/icon-vote';
import { IconWarning } from './svg-icons/icon-warning';
import { IconWithdraw } from './svg-icons/icon-withdraw';
import { IconMan } from './svg-icons/icon-man';
import { IconTeam } from './svg-icons/icon-team';
import { IconStreak } from './svg-icons/icon-streak';
export enum VegaIconNames {
ARROW_DOWN = 'arrow-down',
@@ -62,7 +59,7 @@ export enum VegaIconNames {
CROSS = 'cross',
DEPOSIT = 'deposit',
EDIT = 'edit',
EXCLAMATION_MARK = 'exclamation-mark',
EXCLAIMATION_MARK = 'exclaimation-mark',
EYE = 'eye',
EYE_OFF = 'eye-off',
FORUM = 'forum',
@@ -79,7 +76,6 @@ export enum VegaIconNames {
QUESTION_MARK = 'question-mark',
SEARCH = 'search',
STAR = 'star',
STREAK = 'streak',
SUN = 'sun',
TICK = 'tick',
TICKET = 'ticket',
@@ -90,8 +86,6 @@ export enum VegaIconNames {
VOTE = 'vote',
WITHDRAW = 'withdraw',
WARNING = 'warning',
MAN = 'man',
TEAM = 'team',
}
export const VegaIconNameMap: Record<
@@ -108,7 +102,7 @@ export const VegaIconNameMap: Record<
'chevron-right': IconChevronRight,
'chevron-up': IconChevronUp,
'eye-off': IconEyeOff,
'exclamation-mark': IconExclamationMark,
'exclaimation-mark': IconExclaimationMark,
'open-external': IconOpenExternal,
'question-mark': IconQuestionMark,
'trend-down': IconTrendDown,
@@ -141,7 +135,4 @@ export const VegaIconNameMap: Record<
vote: IconVote,
withdraw: IconWithdraw,
warning: IconWarning,
man: IconMan,
team: IconTeam,
streak: IconStreak,
};
+13
View File
@@ -4,6 +4,7 @@ import {
shorten,
titlefy,
stripFullStops,
ensureSuffix,
} from './strings';
describe('truncateByChars', () => {
@@ -88,3 +89,15 @@ describe('stripFullStops', () => {
});
});
});
describe('ensureSuffix', () => {
it.each([
['', 'abc', 'abc'],
['abc', '', 'abc'],
['def', 'abc', 'abcdef'],
['ąę', 'ae', 'aeąę'],
['🥪', '🍞+🔪=', '🍞+🔪=🥪'],
])('ensures "%s" at the end of "%s": "%s"', (suffix, input, expected) => {
expect(ensureSuffix(input, suffix)).toEqual(expected);
});
});
+6
View File
@@ -33,3 +33,9 @@ export function titlefy(words: (string | null | undefined)[]) {
export function stripFullStops(input: string) {
return input.replace(/\./g, '');
}
export function ensureSuffix(input: string, suffix: string) {
const maybeSuffix = input.substring(input.length - suffix.length);
if (maybeSuffix === suffix) return input;
return input + suffix;
}