feat(trading): referrals (Mk1) (#4816)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Art 2023-09-21 15:25:19 +02:00 committed by GitHub
parent b818e9b2a1
commit 44434a7d39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1781 additions and 81 deletions

View File

@ -24,6 +24,7 @@ NX_STOP_ORDERS=true
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

View File

@ -24,6 +24,7 @@ NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_REFERRALS=false
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket

View File

@ -22,6 +22,7 @@ NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket

View File

@ -27,6 +27,7 @@ NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket

View File

@ -24,6 +24,7 @@ NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_REFERRALS=false
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket

View File

@ -24,3 +24,4 @@ NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true

View File

@ -24,6 +24,7 @@ NX_STOP_ORDERS=true
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

View File

@ -25,6 +25,7 @@ NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_REFERRALS=false
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket

View File

@ -28,7 +28,6 @@ export const useColumnDefs = () => {
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
flex: 2,
cellRenderer: ({
value,
data,
@ -50,7 +49,6 @@ export const useColumnDefs = () => {
{
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
flex: 2,
},
{
headerName: t('Trading mode'),

View File

@ -0,0 +1,102 @@
import {
Input,
InputError,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { FieldValues } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Button } from './buttons';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const ApplyCodeForm = () => {
const [finalized, setFinalized] = useState<boolean>(false);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const {
register,
handleSubmit,
formState: { errors },
setValue,
setError,
} = useForm();
const [params] = useSearchParams();
useEffect(() => {
const code = params.get('code');
if (code) setValue('code', code);
}, [params, setValue]);
const onSubmit = ({ code }: FieldValues) => {
if (isReadOnly || !pubKey || !code || code.length === 0) {
return;
}
sendTx(pubKey, {
applyReferralCode: {
id: code as string,
},
})
.then((res) => {
setFinalized(true);
})
.catch((err) => {
setError('code', {
type: 'required',
message: 'Your code has been rejected',
});
});
};
if (finalized) {
return (
<div className="w-1/2 mx-auto">
<h3 className="mb-5 text-xl text-center uppercase calt flex flex-row gap-2 justify-center items-center">
<span className="text-vega-green-500">
<VegaIcon name={VegaIconNames.TICK} size={20} />
</span>{' '}
<span className="pt-1">Code applied</span>
</h3>
</div>
);
}
return (
<div className="w-1/2 mx-auto">
<h3 className="mb-5 text-xl text-center uppercase calt">
Apply a referral code
</h3>
<p className="mb-6 text-center">Enter a referral code</p>
<form
className={classNames('w-full flex flex-col gap-3', {
'animate-shake': Boolean(errors.code),
})}
onSubmit={handleSubmit(onSubmit)}
>
<label className="flex-grow">
<span className="block mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
Your referral code
</span>
<Input
hasError={Boolean(errors.code)}
{...register('code', {
required: 'You have to provide a code to apply it.',
})}
/>
</label>
<Button
disabled={isReadOnly || !pubKey}
className="w-full"
type="submit"
>
Apply
</Button>
</form>
{errors.code && (
<InputError>{errors.code.message?.toString()}</InputError>
)}
</div>
);
};

View File

@ -0,0 +1,83 @@
import { Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ComponentProps, ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react';
import { NavLink } from 'react-router-dom';
type RainbowButtonProps = {
variant?: 'full' | 'border';
};
export const RainbowButton = ({
variant = 'full',
children,
className,
...props
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow hover:bg-none hover:bg-rainbow enabled:hover:bg-vega-pink-500 rounded-lg overflow-hidden disabled:opacity-40',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
},
className
)}
{...props}
>
<div
className={classNames({
'bg-white dark:bg-vega-cdark-900 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
})}
>
{children}
</div>
</button>
);
const RAINBOW_TAB_STYLE = classNames(
'inline-block',
'bg-vega-clight-500 dark:bg-vega-cdark-500 hover:bg-vega-clight-400 dark:hover:bg-vega-cdark-400',
'data-[state="active"]:text-white data-[state="active"]:bg-rainbow data-[state="active"]:hover:bg-none data-[state="active"]:hover:bg-vega-pink-500 dark:data-[state="active"]:hover:bg-vega-pink-500',
'[&.active]:text-white [&.active]:bg-rainbow [&.active]:hover:bg-none [&.active]:hover:bg-vega-pink-500 dark:[&.active]:hover:bg-vega-pink-500',
'px-5 py-3',
'first:rounded-tl-lg last:rounded-tr-lg'
);
export const RainbowTabButton = forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement>
>(({ children, ...props }, ref) => (
<button ref={ref} className={RAINBOW_TAB_STYLE} {...props}>
{children}
</button>
));
RainbowTabButton.displayName = 'RainbowTabButton';
export const RainbowTabLink = ({
to,
children,
...props
}: ComponentProps<typeof NavLink>) => (
<NavLink to={to} className={RAINBOW_TAB_STYLE} {...props}>
{children}
</NavLink>
);
export const Button = forwardRef<
HTMLButtonElement,
ComponentProps<typeof TradingButton>
>(({ children, intent, type, ...props }, ref) => {
return (
<TradingButton
ref={ref}
intent={intent || type === 'submit' ? Intent.Primary : Intent.None}
type={type}
{...props}
>
{children}
</TradingButton>
);
});
Button.displayName = 'TradingButton';

View File

@ -0,0 +1,6 @@
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';
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';

View File

@ -0,0 +1,225 @@
import {
useVegaWallet,
useVegaWalletDialogStore,
determineId,
} from '@vegaprotocol/wallet';
import { RainbowButton } from './buttons';
import { useState } from 'react';
import {
CopyWithTooltip,
Dialog,
ExternalLink,
InputError,
Intent,
TradingAnchorButton,
TradingButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
export const CreateCodeContainer = () => {
const { stakeAvailable, requiredStake } = useStakeAvailable();
if (stakeAvailable == null || requiredStake == null) {
return null;
}
return (
<CreateCodeForm
currentStakeAvailable={stakeAvailable}
requiredStake={requiredStake}
/>
);
};
export const CreateCodeForm = ({
currentStakeAvailable,
requiredStake,
}: {
currentStakeAvailable: bigint;
requiredStake: bigint;
}) => {
const [dialogOpen, setDialogOpen] = useState(false);
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const { pubKey } = useVegaWallet();
return (
<div className="w-1/2 mx-auto">
<h3 className="mb-5 text-xl text-center uppercase calt">
Create a referral code
</h3>
<p className="mb-6 text-center">
Generate a referral code to share with your friends and start earning
commission.
</p>
<div className="mb-5">
<div className="text-center">
<RainbowButton
variant="border"
onClick={() => {
if (pubKey) {
setDialogOpen(true);
} else {
openWalletDialog();
}
}}
>
{pubKey ? 'Create a referral code' : 'Connect wallet'}
</RainbowButton>
</div>
</div>
<Dialog
title="Create a referral code"
open={dialogOpen}
onChange={() => setDialogOpen(false)}
size="small"
>
<CreateCodeDialog
currentStakeAvailable={currentStakeAvailable}
setDialogOpen={setDialogOpen}
requiredStake={requiredStake}
/>
</Dialog>
</div>
);
};
const CreateCodeDialog = ({
setDialogOpen,
currentStakeAvailable,
requiredStake,
}: {
setDialogOpen: (open: boolean) => void;
currentStakeAvailable: bigint;
requiredStake: bigint;
}) => {
const createLink = useLinks(DApp.Governance);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const [err, setErr] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null);
const [status, setStatus] = useState<
'idle' | 'loading' | 'success' | 'error'
>('idle');
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
} else {
setErr(null);
setStatus('loading');
setCode(null);
sendTx(pubKey, {
createReferralSet: {
isTeam: false,
},
})
.then((res) => {
if (!res) {
setErr(`Invalid response: ${JSON.stringify(res)}`);
return;
}
const code = determineId(res.signature);
setCode(code);
setStatus('success');
})
.catch((err) => {
if (err.message.includes('user rejected')) {
setStatus('idle');
return;
}
setStatus('error');
setErr(err.message);
});
}
};
const getButtonProps = () => {
if (status === 'idle' || status === 'error') {
return {
children: 'Generate code',
onClick: () => onSubmit(),
};
}
if (status === 'loading') {
return {
children: 'Confirm in wallet...',
disabled: true,
};
}
if (status === 'success') {
return {
children: 'Close',
intent: Intent.Success,
onClick: () => setDialogOpen(false),
};
}
};
// TODO: Add when network parameters are updated
if (
currentStakeAvailable === BigInt(0) ||
currentStakeAvailable < requiredStake
) {
return (
<div className="flex flex-col gap-4">
<p>
You need at least{' '}
{addDecimalsFormatNumber(requiredStake.toString(), 18)} VEGA staked to
generate a referral code and participate in the referral program.
</p>
<TradingAnchorButton
href={createLink(TokenStaticLinks.ASSOCIATE)}
intent={Intent.Primary}
target="_blank"
>
Stake some $VEGA now
</TradingAnchorButton>
</div>
);
}
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
<p>
Generate a referral code to share with your friends and start earning
commission.
</p>
)}
{status === 'success' && code && (
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
{code}
</p>
</div>
<CopyWithTooltip text={code}>
<TradingButton
className="text-sm no-underline"
icon={<VegaIcon name={VegaIconNames.COPY} />}
>
<span>Copy</span>
</TradingButton>
</CopyWithTooltip>
</div>
)}
<TradingButton
fill={true}
intent={Intent.Primary}
{...getButtonProps()}
/>
{err && <InputError>{err}</InputError>}
{/* TODO: Add links */}
<div className="flex justify-center pt-5 mt-2 text-sm border-t gap-4 text-default border-default">
<ExternalLink>About the referral program</ExternalLink>
<ExternalLink>Disclaimer</ExternalLink>
</div>
</div>
);
};

View File

@ -0,0 +1,77 @@
import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router';
import { RainbowButton } from './buttons';
import { AnimatedDudeWithWire } from './graphics/dude';
import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links';
export const ErrorBoundary = () => {
const error = useRouteError();
const navigate = useNavigate();
const title = isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: 'Something went wrong';
const code = isRouteErrorResponse(error) ? error.status : 0;
const messages: Record<number, string> = {
0: 'An unknown error occurred.',
404: "The page you're looking for doesn't exists.",
};
return (
<LayoutWithSky className="pt-32">
<div
aria-hidden
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
>
<AnimatedDudeWithWire className="animate-spin" />
</div>
<h1 className="text-6xl font-alpha calt mb-10">{title}</h1>
{Object.keys(messages).includes(code.toString()) ? (
<p className="text-lg mb-10">{messages[code]}</p>
) : null}
<p className="text-lg mb-10">
<RainbowButton
onClick={() => navigate('..')}
variant="border"
className="text-xs"
>
Go back and try again
</RainbowButton>
</p>
</LayoutWithSky>
);
};
export const NotFound = () => {
const navigate = useNavigate();
return (
<div className="pt-32">
<div
aria-hidden
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
>
<AnimatedDudeWithWire className="animate-spin" />
</div>
<h1 className="text-6xl font-alpha calt mb-10">{'Not found'}</h1>
<p className="text-lg mb-10">
{"The page you're looking for doesn't exists."}
</p>
<p className="text-lg mb-10">
<RainbowButton
onClick={() => navigate(Routes.REFERRALS)}
variant="border"
className="text-xs"
>
Go back and try again
</RainbowButton>
</p>
</div>
);
};

View File

@ -0,0 +1,70 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
export const Dude = ({ className }: HTMLAttributes<SVGElement>) => {
return (
<svg
width="41"
height="47"
viewBox="0 0 41 47"
fill="none"
className={className}
>
<path
d="M21.1895 0.298767L5.08827 27.4101L8.96133 29.7103L4.36099 37.4564L8.23404 39.7566L12.8344 32.0105L16.7074 34.3107L12.1071 42.0568L15.9801 44.3569L20.5805 36.6108L24.4535 38.911L40.5547 11.7996L21.1895 0.298767Z"
className="fill-black dark:fill-white"
/>
<path
d="M35.9346 15.1683L20.4424 5.96765L14.3086 16.2958L29.8008 25.4965L35.9346 15.1683Z"
className="fill-white dark:fill-black"
/>
<path
d="M25.646 17.7895L23.064 16.2561L21.5305 18.8381L24.1126 20.3716L25.646 17.7895Z"
className="fill-black dark:fill-white"
/>
<path
d="M29.7612 16.7412L27.1792 15.2077L25.6458 17.7898L28.2278 19.3232L29.7612 16.7412Z"
className="fill-black dark:fill-white"
/>
<path
d="M33.877 15.6925L31.2949 14.159L29.7615 16.7411L32.3435 18.2745L33.877 15.6925Z"
className="fill-black dark:fill-white"
/>
<path
d="M29.0342 26.7874L26.4521 25.2539L24.9187 27.836L27.5007 29.3694L29.0342 26.7874Z"
fill="#FF077F"
/>
</svg>
);
};
export const Wire = ({ className }: HTMLAttributes<SVGElement>) => {
return (
<svg
width="157"
height="88"
viewBox="0 0 157 88"
fill="none"
className={className}
>
<path
d="M109.398 6.12235C127.37 -3.81898 146.791 1.45045 153.465 14.307C160.138 27.1636 154.195 43.9948 140.438 52.1164C126.68 60.238 105.767 54.9998 84.9212 43.464C64.0752 31.9281 32.2412 6.42016 18.8175 24.185C6.90871 40.719 41.9332 68.4495 29.2664 82.7049C23.187 88.4974 11.1379 88.2645 0.968295 80.3398"
className="stroke-black dark:stroke-white"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
</svg>
);
};
export const AnimatedDudeWithWire = ({ className }: { className?: string }) => (
<div className="relative">
<Wire className="absolute top-[25px]" />
<Dude
className={classNames(
'absolute left-[96px] animate-[wave_20s_ease-in-out_infinite]',
className
)}
/>
</div>
);

View File

@ -0,0 +1,133 @@
import { gql, useQuery } from '@apollo/client';
import { getNumberFormat } from '@vegaprotocol/utils';
import { addDays } from 'date-fns';
import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit';
// TODO: Generate query
// eslint-disable-next-line
const REFERRAL_PROGRAM_QUERY = gql`
query ReferralProgram {
currentReferralProgram {
id
version
endOfProgramTimestamp
windowLength
endedAt
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
`;
const STAKING_TIERS_MAPPING: Record<number, string> = {
1: 'Tradestarter',
2: 'Mid level degen',
3: 'Reward hoarder',
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MOCK = {
data: {
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',
referralRewardFactor: '0.05',
},
{
minimumEpochs: 5,
minimumRunningNotionalTakerVolume: '10000',
referralDiscountFactor: '0.001',
referralRewardFactor: '0.001',
},
],
stakingTiers: [
{
minimumStakedTokens: '10000',
referralRewardMultiplier: '1',
},
{
minimumStakedTokens: '20000',
referralRewardMultiplier: '2',
},
{
minimumStakedTokens: '30000',
referralRewardMultiplier: '3',
},
],
},
},
loading: false,
error: undefined,
};
export const useReferralProgram = () => {
const { data, loading, error } = useQuery(REFERRAL_PROGRAM_QUERY, {
fetchPolicy: 'cache-and-network',
});
if (!data) {
return {
benefitTiers: [],
stakingTiers: [],
details: undefined,
loading,
error,
};
}
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
Number(t.referralRewardFactor)
)
.reverse()
.map((t, i) => {
return {
tier: i + 1,
commission: Number(t.referralRewardFactor) * 100 + '%',
discount: Number(t.referralDiscountFactor) * 100 + '%',
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
};
});
const stakingTiers = sortBy(
data.currentReferralProgram.stakingTiers,
(t) => t.referralRewardMultiplier
).map((t, i) => {
return {
tier: i + 1,
label: STAKING_TIERS_MAPPING[i + 1],
...t,
};
});
return {
benefitTiers,
stakingTiers,
details: omit(data.currentReferralProgram, 'benefitTiers', 'stakingTiers'),
loading,
error,
};
};

View File

@ -0,0 +1,114 @@
import { gql, useQuery } from '@apollo/client';
import { removePaginationWrapper } from '@vegaprotocol/utils';
const REFERRER_QUERY = gql`
query ReferralSets($partyId: ID!) {
referralSets(referrer: $partyId) {
edges {
node {
id
referrer
createdAt
updatedAt
}
}
}
}
`;
const REFEREE_QUERY = gql`
query ReferralSets($partyId: ID!) {
referralSets(referee: $partyId) {
edges {
node {
id
referrer
createdAt
updatedAt
}
}
}
}
`;
const REFEREES_QUERY = gql`
query ReferralSets($code: ID!) {
referralSetReferees(id: $code) {
edges {
node {
referralSetId
refereeId
joinedAt
atEpoch
}
}
}
}
`;
// TODO: generate types after perps work is merged
export type ReferralData = {
code: string;
referees: Array<{
refereeId: string;
joinedAt: string;
atEpoch: number;
}>;
};
export const useReferral = (
pubKey: string | null,
role: 'referrer' | 'referee'
) => {
const query = {
referrer: REFERRER_QUERY,
referee: REFEREE_QUERY,
};
const {
data: referralData,
loading: referralLoading,
error: referralError,
} = useQuery(query[role], {
variables: {
partyId: pubKey,
},
skip: !pubKey,
fetchPolicy: 'cache-and-network',
});
// A user can only have 1 active referral program at a time
const referral = referralData?.referralSets.edges.length
? referralData.referralSets.edges[0].node
: undefined;
const {
data: refereesData,
loading: refereesLoading,
error: refereesError,
} = useQuery(REFEREES_QUERY, {
variables: {
code: referral?.id,
},
skip: !referral?.id,
fetchPolicy: 'cache-and-network',
});
const referees = removePaginationWrapper(
refereesData?.referralSetReferees.edges
);
const data =
referral && refereesData
? {
code: referral.id,
referees,
}
: undefined;
return {
data: data as ReferralData | undefined,
loading: referralLoading || refereesLoading,
error: referralError || refereesError,
};
};

View File

@ -0,0 +1,34 @@
import { gql, useQuery } from '@apollo/client';
import { useVegaWallet } from '@vegaprotocol/wallet';
const STAKE_QUERY = gql`
query CreateCode($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
export const useStakeAvailable = () => {
const { pubKey } = useVegaWallet();
const { data } = useQuery(STAKE_QUERY, {
variables: { partyId: pubKey || '' },
skip: !pubKey,
// TODO: remove when network params available
errorPolicy: 'ignore',
});
return {
stakeAvailable: data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined,
requiredStake: data
? BigInt(data.networkParameter?.value || '0')
: undefined,
};
};

View File

@ -0,0 +1,52 @@
import { Table } from './table';
export const HowItWorksTable = () => (
<Table
className="bg-none bg-vega-clight-800 dark:bg-vega-cdark-800"
noHeader
noCollapse
columns={[{ name: 'number', className: 'pr-0' }, { name: 'step' }]}
data={[
{
number: (
<span className="text-2xl calt text-vega-clight-100 dark:text-vega-cdark-100">
1
</span>
),
step: 'Referrers generate a code assigned to their key via an on chain transaction',
},
{
number: (
<span className="text-2xl calt text-vega-clight-100 dark:text-vega-cdark-100">
2
</span>
),
step: 'Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction',
},
{
number: (
<span className="text-2xl calt text-vega-clight-100 dark:text-vega-cdark-100">
3
</span>
),
step: 'Discounts are applied automatically during trading based on the key(s) used',
},
{
number: (
<span className="text-2xl calt text-vega-clight-100 dark:text-vega-cdark-100">
4
</span>
),
step: 'Referrers earn commission based on a percentage of the taker fees their referees pay',
},
{
number: (
<span className="text-2xl calt text-vega-clight-100 dark:text-vega-cdark-100">
5
</span>
),
step: 'The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee',
},
]}
></Table>
);

View File

@ -0,0 +1,31 @@
import classNames from 'classnames';
import { AnimatedDudeWithWire } from './graphics/dude';
export const LandingBanner = () => {
return (
<div className={classNames('relative mb-20')}>
<div className="">
<div
aria-hidden
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
>
<AnimatedDudeWithWire />
</div>
<div className="pt-32 sm:w-[50%]">
<h1 className="text-6xl font-alpha calt mb-10">
Earn commission & stake rewards
</h1>
<p className="text-lg mb-10">
Invite friends and earn commission in the form of Vega rewards from
the trading fees they pay. Stake those rewards to earn multipliers
on future rewards.
</p>
<p className="text-lg">
Any friends that join using the code will receive discounts off
trading fees.
</p>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,35 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { SKY_BACKGROUND } from './constants';
import { Outlet } from 'react-router-dom';
export const Layout = ({
className,
children,
...props
}: HTMLAttributes<HTMLDivElement>) => {
return (
<div
className={classNames(
'max-w-[1440px]',
'mx-auto px-16 md:px-32 pb-32',
'relative z-0',
className
)}
{...props}
>
{children || <Outlet />}
</div>
);
};
export const LayoutWithSky = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => {
return (
<div className={classNames('h-full overflow-auto', SKY_BACKGROUND)}>
<Layout className={className} {...props} />
</div>
);
};

View File

@ -0,0 +1,113 @@
import { Tile } from './tile';
import {
CopyWithTooltip,
Input,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Button, RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import type { ReferralData } from './hooks/use-referral';
import { useReferral } from './hooks/use-referral';
import { CreateCodeContainer } from './create-code-form';
import classNames from 'classnames';
const CodeTile = ({
code,
as,
}: {
code: string;
as: 'referrer' | 'referee';
}) => {
return (
<Tile variant="rainbow">
<h3 className="mb-1 text-lg calt">Your referral code</h3>
{as === 'referrer' && (
<p className="mb-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
Share this code with friends
</p>
)}
<div className="flex gap-2">
<Input size={1} readOnly value={code} />
<CopyWithTooltip text={code}>
<Button
className="text-sm no-underline"
icon={<VegaIcon name={VegaIconNames.COPY} />}
>
<span>Copy</span>
</Button>
</CopyWithTooltip>
</div>
</Tile>
);
};
export const ReferralStatistics = () => {
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const { pubKey } = useVegaWallet();
const { data: referee } = useReferral(pubKey, 'referee');
const { data: referrer } = useReferral(pubKey, 'referrer');
if (!pubKey) {
return (
<div className="text-center">
<RainbowButton variant="border" onClick={() => openWalletDialog()}>
Connect wallet
</RainbowButton>
</div>
);
}
if (referee?.code) {
return <Statistics data={referee} as="referee" />;
}
if (referrer?.code) {
return <Statistics data={referrer} as="referrer" />;
}
return <CreateCodeContainer />;
};
const Statistics = ({
data,
as,
}: {
data: ReferralData;
as: 'referrer' | 'referee';
}) => {
return (
<div
className={classNames('grid grid-cols-1 grid-rows-1 gap-5 mx-auto', {
'md:w-1/2': as === 'referee',
'md:w-2/3': as === 'referrer',
})}
>
<div
className={classNames('grid grid-rows-1 gap-5', {
'grid-cols-2': as === 'referrer',
'grid-cols-1': as === 'referee',
})}
>
{as === 'referrer' && data?.referees && (
<Tile className="py-3 h-full">
<div className="absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%]">
<h3 className="mb-1 text-6xl text-center">
{data.referees.length}
</h3>
<p className="text-sm text-center text-vega-clight-100 dark:text-vega-cdark-100">
{data.referees.length === 1
? 'Trader referred'
: 'Total traders referred'}
</p>
</div>
</Tile>
)}
<CodeTile code={data?.code} as={as} />
</div>
</div>
);
};

View File

@ -0,0 +1,50 @@
import {
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { HowItWorksTable } from './how-it-works-table';
import { LandingBanner } from './landing-banner';
import { TiersContainer } from './tiers';
import { RainbowTabLink } from './buttons';
import { Outlet } from 'react-router-dom';
import { Routes } from '../../lib/links';
export const Referrals = () => {
return (
<>
<LandingBanner />
<div>
<div className="flex justify-center">
<RainbowTabLink end to={Routes.REFERRALS}>
Your referrals
</RainbowTabLink>
<RainbowTabLink to={Routes.REFERRALS_APPLY_CODE}>
Apply a code
</RainbowTabLink>
</div>
<div className="py-16 border-t border-b border-vega-cdark-500">
<Outlet />
</div>
</div>
<TiersContainer />
<div className="mt-10 mb-5 text-center">
<h2 className="text-2xl">How it works</h2>
</div>
<div className="md:w-[60%] mx-auto">
<HowItWorksTable />
<div className="mt-5">
<TradingAnchorButton
className="mx-auto w-max"
href="https://docs.vega.xyz/"
target="_blank"
>
Read the terms <VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</TradingAnchorButton>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,116 @@
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { BORDER_COLOR, GRADIENT } from './constants';
type TableColumnDefinition = {
displayName?: string;
name: string;
tooltip?: string;
className?: string;
};
type TableProps = {
columns: TableColumnDefinition[];
data: Record<TableColumnDefinition['name'] | 'className', React.ReactNode>[];
noHeader?: boolean;
noCollapse?: boolean;
};
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
export const Table = ({
columns,
data,
noHeader = false,
noCollapse = false,
className,
...props
}: TableProps & HTMLAttributes<HTMLTableElement>) => {
const header = (
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
<tr>
{columns.map(({ displayName, name, tooltip }) => (
<th
key={name}
col-id={name}
className={classNames(
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100',
INNER_BORDER_STYLE
)}
>
<span className="flex flex-row gap-2 items-center">
<span>{displayName}</span>
{tooltip ? (
<Tooltip description={tooltip}>
<button className="text-vega-clight-400 dark:text-vega-cdark-400 no-underline decoration-transparent w-[12px] h-[12px] inline-flex">
<VegaIcon size={12} name={VegaIconNames.INFO} />
</button>
</Tooltip>
) : null}
</span>
</th>
))}
</tr>
</thead>
);
return (
<table
className={classNames(
'w-full',
'border-separate border rounded-md border-spacing-0',
BORDER_COLOR,
GRADIENT,
className
)}
{...props}
>
{!noHeader && header}
<tbody>
{data.map((d, i) => (
<tr
key={i}
className={classNames(d['className'] as string, {
'max-md:flex flex-col w-full': !noCollapse,
})}
>
{columns.map(({ name, displayName, className }, j) => (
<td
className={classNames(
'px-5 py-3 text-base',
{
'max-md:flex max-md:flex-col max-md:justify-between':
!noCollapse,
},
INNER_BORDER_STYLE,
{
'border-none': i === data.length - 1 && noCollapse,
'md:border-none': i === data.length - 1,
'max-md:border-none':
i === data.length - 1 && j === columns.length - 1,
},
className
)}
key={`${i}-${name}`}
>
{/** display column name in mobile view */}
{!noCollapse &&
!noHeader &&
displayName &&
displayName.length > 0 && (
<span
aria-hidden
className="md:hidden font-mono text-xs px-0 text-vega-clight-100 dark:text-vega-cdark-100"
>
{displayName}
</span>
)}
<span>{d[name]}</span>
</td>
))}
</tr>
))}
</tbody>
</table>
);
};

View File

@ -0,0 +1,32 @@
import type { HTMLAttributes } from 'react';
import classNames from 'classnames';
type TagProps = {
color?: 'yellow' | 'green' | 'blue' | 'purple' | 'pink' | 'orange' | 'none';
};
export const Tag = ({
color = 'none',
children,
className,
...props
}: TagProps & HTMLAttributes<HTMLDivElement>) => (
<div
className={classNames(
'mt-3 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-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',
'border-vega-orange-500 text-vega-orange-500': color === 'orange',
'border-vega-clight-100 text-vega-clight-100 dark:border-vega-cdark-100 dark:text-vega-cdark-100':
color === 'none',
},
className
)}
{...props}
>
{children}
</div>
);

View File

@ -0,0 +1,172 @@
import { 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 } from 'react';
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
<div
className={classNames(
'bg-vega-clight-800 dark:bg-vega-cdark-800 rounded-lg animate-pulse',
{
'w-full h-20': variant === 'large',
}
)}
></div>
);
const StakingTier = ({
tier,
label,
referralRewardMultiplier,
minimumStakedTokens,
}: {
tier: number;
label: string;
referralRewardMultiplier: string;
minimumStakedTokens: string;
}) => {
const color: Record<number, ComponentProps<typeof Tag>['color']> = {
1: 'green',
2: 'blue',
3: 'pink',
};
return (
<div
className={classNames(
'overflow-hidden',
'border rounded-md w-full',
BORDER_COLOR
)}
>
<div aria-hidden>
{tier < 4 && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={`/${tier}x.png`}
alt={`${referralRewardMultiplier}x multiplier`}
width={768}
height={400}
className="w-full"
/>
)}
</div>
<div className={classNames('p-3', GRADIENT)}>
<h3 className="mb-3 text-xl">{label}</h3>
<p className="text-base text-vega-clight-100 dark:text-vega-cdark-100">
Stake a minimum of {minimumStakedTokens} $VEGA tokens
</p>
<Tag color={color[tier]}>
Reward multiplier {referralRewardMultiplier}x
</Tag>
</div>
</div>
);
};
export const TiersContainer = () => {
const { benefitTiers, stakingTiers, details, loading } = useReferralProgram();
const ends = details?.endOfProgramTimestamp
? getDateTimeFormat().format(new Date(details.endOfProgramTimestamp))
: undefined;
return (
<>
<div className="flex flex-row items-baseline justify-between mt-10 mb-5">
<h2 className="text-2xl">Referral tiers</h2>
{ends && (
<span className="text-base">
<span className="text-vega-clight-200 dark:text-vega-cdark-200">
Program ends:
</span>{' '}
{ends}
</span>
)}
</div>
<div className="mb-20">
{loading || !benefitTiers || benefitTiers.length === 0 ? (
<Loading variant="large" />
) : (
<TiersTable data={benefitTiers} />
)}
</div>
<div className="flex flex-row items-baseline justify-between mb-5">
<h2 className="text-2xl">Staking multipliers</h2>
</div>
<div className="flex flex-col mb-20 justify-items-stretch md:flex-row gap-5">
{loading || !stakingTiers || stakingTiers.length === 0 ? (
<>
<Loading variant="large" />
<Loading variant="large" />
<Loading variant="large" />
</>
) : (
<StakingTiers data={stakingTiers} />
)}
</div>
</>
);
};
const StakingTiers = ({
data,
}: {
data: ReturnType<typeof useReferralProgram>['stakingTiers'];
}) => (
<>
{data.map(
({ tier, label, referralRewardMultiplier, minimumStakedTokens }, i) => (
<StakingTier
key={i}
tier={tier}
label={label}
referralRewardMultiplier={referralRewardMultiplier}
minimumStakedTokens={minimumStakedTokens}
/>
)
)}
</>
);
const TiersTable = ({
data,
}: {
data: Array<{
tier: number;
commission: string;
discount: string;
volume: string;
}>;
}) => {
return (
<Table
columns={[
{ name: 'tier', displayName: 'Tier' },
{
name: 'commission',
displayName: 'Referrer commission',
tooltip: 'A percentage of commission earned by the referrer',
},
{ name: 'discount', displayName: 'Referrer trading discount' },
{ name: 'volume', displayName: 'Min. trading volume' },
]}
data={data.map((d) => ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier === 1,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 3,
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier > 3,
}),
}))}
/>
);
};

View File

@ -0,0 +1,39 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import { BORDER_COLOR, GRADIENT } from './constants';
type TileProps = {
variant?: 'rainbow' | 'default';
};
export const Tile = ({
variant = 'default',
className,
children,
}: TileProps & HTMLAttributes<HTMLDivElement>) => {
return (
<div
className={classNames(
{
'bg-rainbow p-[0.125rem]': variant === 'rainbow',
[`border-2 ${BORDER_COLOR} p-0`]: variant === 'default',
},
'rounded-lg overflow-hidden relative'
)}
>
<div
className={classNames(
{
'bg-white dark:bg-vega-cdark-900 text-black dark:text-white rounded-[0.35rem] overflow-hidden':
variant === 'rainbow',
},
'p-6',
GRADIENT,
className
)}
>
{children}
</div>
</div>
);
};

View File

@ -5,6 +5,7 @@ import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { Navbar } from './navbar';
import { useGlobalStore } from '../../stores';
import { ENV, FLAGS } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/proposals', () => ({
ProtocolUpgradeCountdown: () => null,
@ -47,6 +48,14 @@ describe('Navbar', () => {
beforeAll(() => {
useGlobalStore.setState({ marketId });
const mockedFLAGS = jest.mocked(FLAGS);
mockedFLAGS.REFERRALS = true;
const mockedENV = jest.mocked(ENV);
mockedENV.VEGA_TOKEN_URL = 'governance';
});
afterAll(() => {
jest.clearAllMocks();
});
it('should be properly rendered', () => {
@ -57,6 +66,7 @@ describe('Navbar', () => {
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'],
[expect.stringContaining('governance'), 'Governance'],
];
@ -89,6 +99,7 @@ describe('Navbar', () => {
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
['/referrals', 'Referrals'],
[expect.stringContaining('governance'), 'Governance'],
];
const links = menu.getAllByRole('link');

View File

@ -6,6 +6,7 @@ import {
Networks,
DApp,
useLinks,
FLAGS,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useGlobalStore } from '../../stores';
@ -140,10 +141,6 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
const { VEGA_ENV, VEGA_NETWORKS, GITHUB_FEEDBACK_URL } = useEnvironment();
const marketId = useGlobalStore((store) => store.marketId);
// If we have a stored marketId make Trade link go to that market
// otherwise always go to /markets/all
const tradingPath = marketId ? Links.MARKET(marketId) : Links.MARKETS();
return (
<div className="lg:flex lg:h-full gap-3">
<NavbarList>
@ -174,7 +171,7 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLink to={tradingPath} onClick={onClick}>
<NavbarLink to={Links.MARKET(marketId || '')} onClick={onClick}>
{t('Trading')}
</NavbarLink>
</NavbarItem>
@ -183,6 +180,13 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Portfolio')}
</NavbarLink>
</NavbarItem>
{FLAGS.REFERRALS && (
<NavbarItem>
<NavbarLink to={Links.REFERRALS()} onClick={onClick}>
{t('Referrals')}
</NavbarLink>
</NavbarItem>
)}
<NavbarItem>
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
{t('Governance')}

View File

@ -13,6 +13,10 @@ export const Routes = {
DEPOSIT: '/portfolio/assets/deposit',
WITHDRAW: '/portfolio/assets/withdraw',
TRANSFER: '/portfolio/assets/transfer',
REFERRALS: '/referrals',
REFERRALS_APPLY_CODE: '/referrals/apply-code',
REFERRALS_CREATE_CODE: '/referrals/create-code',
TEAMS: '/teams',
} as const;
type ConsoleLinks = {
@ -32,4 +36,8 @@ export const Links: ConsoleLinks = {
DEPOSIT: () => Routes.DEPOSIT,
WITHDRAW: () => Routes.WITHDRAW,
TRANSFER: () => Routes.TRANSFER,
REFERRALS: () => Routes.REFERRALS,
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
TEAMS: () => Routes.TEAMS,
};

View File

@ -14,6 +14,14 @@ import { Deposit } from '../client-pages/deposit';
import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer';
import { Routes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals';
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
import { ApplyCodeForm } from '../client-pages/referrals/apply-code-form';
import { CreateCodeContainer } from '../client-pages/referrals/create-code-form';
import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary';
import { compact } from 'lodash';
import { FLAGS } from '@vegaprotocol/environment';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM
@ -26,8 +34,8 @@ const NotFound = () => (
</Splash>
);
export const routerConfig: RouteObject[] = [
// Pages that dont use the LayoutWithSidebar must come first
export const routerConfig: RouteObject[] = compact([
// Pages that don't use the LayoutWithSidebar must come first
// to ensure they are matched before the catch all route '/*'
{
path: 'disclaimer',
@ -35,7 +43,36 @@ export const routerConfig: RouteObject[] = [
id: Routes.DISCLAIMER,
children: [{ index: true, element: <Disclaimer /> }],
},
// Referrals routing (the pages should be available if the feature flag is on)
FLAGS.REFERRALS
? {
path: Routes.REFERRALS,
element: <LayoutWithSky />,
children: [
{
element: <Referrals />,
children: [
{
index: true,
element: <ReferralStatistics />,
},
{
path: Routes.REFERRALS_CREATE_CODE,
element: <CreateCodeContainer />,
},
{
path: Routes.REFERRALS_APPLY_CODE,
element: <ApplyCodeForm />,
},
],
},
{
path: '*',
element: <ReferralNotFound />,
},
],
}
: undefined,
// All other pages will use the sidebar
{
path: '/*',
@ -93,7 +130,6 @@ export const routerConfig: RouteObject[] = [
element: <Liquidity />,
id: Routes.LIQUIDITY,
},
// NotFound page is here so its caught within parent '/*' route
{
path: '*',
@ -101,7 +137,7 @@ export const routerConfig: RouteObject[] = [
},
],
},
];
]);
export const ClientRouter = () => {
const routes = useRoutes(routerConfig);

BIN
apps/trading/public/1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
apps/trading/public/2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

BIN
apps/trading/public/3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 KiB

View File

@ -13,7 +13,37 @@ module.exports = {
],
darkMode: 'class',
theme: {
extend: theme,
extend: {
...theme,
colors: {
transparent: 'transparent',
current: 'currentColor',
...theme.colors,
},
backgroundImage: {
...theme.backgroundImage,
rainbow:
'linear-gradient(103.47deg, #FF077F 1.68%, #8028FF 47.49%, #0075FF 100%)',
'rainbow-shifted':
'linear-gradient(103.47deg, #0075FF 1.68%, #8028FF 47.49%, #FF077F 100%)',
highlight:
'linear-gradient(170deg, var(--tw-gradient-from), transparent var(--tw-gradient-to-position))',
},
keyframes: {
...theme.keyframes,
shake: {
'0%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(5px)' },
'50%': { transform: 'translateX(-5px)' },
'75%': { transform: 'translateX(5px)' },
'100%': { transform: 'translateX(0)' },
},
},
animation: {
...theme.animation,
shake: 'shake 200ms linear',
},
},
},
plugins: [vegaCustomClasses],
};

View File

@ -414,6 +414,9 @@ function compileFeatureFlags(): FeatureFlags {
process.env['NX_METAMASK_SNAPS']
) as string
),
REFERRALS: TRUTHY.includes(
windowOrDefault('NX_REFERRALS', process.env['NX_REFERRALS']) as string
),
};
const EXPLORER_FLAGS = {
EXPLORER_ASSETS: TRUTHY.includes(

View File

@ -1,7 +1,7 @@
import trim from 'lodash/trim';
import { useCallback } from 'react';
import { Networks } from '../types';
import { useEnvironment } from './use-environment';
import { ENV, useEnvironment } from './use-environment';
import { stripFullStops } from '@vegaprotocol/utils';
const VEGA_DOCS_URL =
@ -84,8 +84,7 @@ export const DocsLinks = VEGA_DOCS_URL
: undefined;
export const useLinks = (dapp: DApp, network?: Net) => {
const { VEGA_ENV, VEGA_EXPLORER_URL, VEGA_TOKEN_URL, VEGA_CONSOLE_URL } =
useEnvironment();
const { VEGA_ENV, VEGA_EXPLORER_URL, VEGA_TOKEN_URL, VEGA_CONSOLE_URL } = ENV;
const fallback = {
[DApp.Explorer]: VEGA_EXPLORER_URL,
[DApp.Governance]: VEGA_TOKEN_URL,
@ -175,4 +174,5 @@ export const ExternalLinks = {
export const TokenStaticLinks = {
PROPOSAL_PAGE: ':tokenUrl/proposals/:proposalId',
UPDATE_PROPOSAL_PAGE: ':tokenUrl/proposals/propose/update-market',
ASSOCIATE: 'token/associate',
};

View File

@ -23,6 +23,7 @@ export type CosmicElevatorFlags = Pick<
| 'SUCCESSOR_MARKETS'
| 'PRODUCT_PERPETUALS'
| 'METAMASK_SNAPS'
| 'REFERRALS'
>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
export const CUSTOM_NODE_KEY = 'custom' as const;

View File

@ -78,6 +78,7 @@ const COSMIC_ELEVATOR_FLAGS = {
ICEBERG_ORDERS: z.optional(z.boolean()),
PRODUCT_PERPETUALS: z.optional(z.boolean()),
METAMASK_SNAPS: z.optional(z.boolean()),
REFERRALS: z.optional(z.boolean()),
};
const EXPLORER_FLAGS = {

View File

@ -395,6 +395,24 @@ export interface TransferBody {
transfer: Transfer;
}
export type ApplyReferralCode = {
applyReferralCode: {
id: string;
};
};
export type CreateReferralSet = {
createReferralSet: {
isTeam: boolean;
team?: {
name: string;
teamUrl?: string;
avatarUrl?: string;
closed: boolean;
};
};
};
export type Transaction =
| StopOrdersSubmissionBody
| StopOrdersCancellationBody
@ -408,7 +426,9 @@ export type Transaction =
| ProposalSubmissionBody
| BatchMarketInstructionSubmissionBody
| TransferBody
| LiquidityProvisionSubmission;
| LiquidityProvisionSubmission
| ApplyReferralCode
| CreateReferralSet;
export const isWithdrawTransaction = (
transaction: Transaction

View File

@ -213,7 +213,7 @@
"style-loader": "^3.3.0",
"stylus": "^0.55.0",
"stylus-loader": "^7.1.0",
"tailwindcss": "3.2.7",
"tailwindcss": "3.3.3",
"ts-jest": "29.1.0",
"ts-node": "10.9.1",
"tslib": "^2.3.0",

222
yarn.lock
View File

@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.0.1.tgz#b38b444ad3aa5fedbb15f2f746dcd934226a12dd"
integrity sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==
"@alloc/quick-lru@^5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
@ -9856,16 +9861,7 @@ acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-node@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==
dependencies:
acorn "^7.0.0"
acorn-walk "^7.0.0"
xtend "^4.0.2"
acorn-walk@^7.0.0, acorn-walk@^7.2.0:
acorn-walk@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
@ -9875,7 +9871,7 @@ acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^7.0.0, acorn@^7.4.1:
acorn@^7.4.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
@ -10126,6 +10122,11 @@ any-observable@^0.3.0:
resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"
integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@^3.0.3, anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
@ -13199,11 +13200,6 @@ define-properties@^1.2.0:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
defined@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==
defu@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.2.tgz#1217cba167410a1765ba93893c6dbac9ed9d9e5c"
@ -13315,15 +13311,6 @@ detect-port@^1.3.0, detect-port@^1.5.1:
address "^1.0.1"
debug "4"
detective@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==
dependencies:
acorn-node "^1.8.2"
defined "^1.0.0"
minimist "^1.2.6"
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@ -15557,6 +15544,18 @@ glob@7.1.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.7, glob@~7.1.1:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
@ -16628,6 +16627,13 @@ is-core-module@^2.11.0:
dependencies:
has "^1.0.3"
is-core-module@^2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
dependencies:
has "^1.0.3"
is-core-module@^2.5.0, is-core-module@^2.8.1:
version "2.10.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
@ -17670,6 +17676,11 @@ jest@29.4.3:
import-local "^3.0.2"
jest-cli "^29.4.3"
jiti@^1.18.2:
version "1.20.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
js-sha3@0.8.0, js-sha3@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
@ -18115,7 +18126,7 @@ lilconfig@^2.0.3, lilconfig@^2.1.0:
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
lilconfig@^2.0.5, lilconfig@^2.0.6:
lilconfig@^2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==
@ -19505,6 +19516,15 @@ mute-stream@0.0.8:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
object-assign "^4.0.1"
thenify-all "^1.0.0"
nanoid@3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
@ -19857,7 +19877,7 @@ nx@16.4.0:
"@nx/nx-win32-arm64-msvc" "16.4.0"
"@nx/nx-win32-x64-msvc" "16.4.0"
object-assign@^4.1.0, object-assign@^4.1.1:
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
@ -20597,6 +20617,11 @@ pino@7.11.0:
sonic-boom "^2.2.1"
thread-stream "^0.15.1"
pirates@^4.0.1:
version "4.0.6"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
pirates@^4.0.4, pirates@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
@ -20746,7 +20771,16 @@ postcss-discard-overridden@^6.0.0:
resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz#49c5262db14e975e349692d9024442de7cd8e234"
integrity sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==
postcss-import@^14.1.0, postcss-import@~14.1.0:
postcss-import@^15.1.0:
version "15.1.0"
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70"
integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
dependencies:
postcss-value-parser "^4.0.0"
read-cache "^1.0.0"
resolve "^1.1.7"
postcss-import@~14.1.0:
version "14.1.0"
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
@ -20755,14 +20789,14 @@ postcss-import@^14.1.0, postcss-import@~14.1.0:
read-cache "^1.0.0"
resolve "^1.1.7"
postcss-js@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00"
integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==
postcss-js@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
dependencies:
camelcase-css "^2.0.1"
postcss-load-config@^3.0.0, postcss-load-config@^3.1.4:
postcss-load-config@^3.0.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
@ -20770,6 +20804,14 @@ postcss-load-config@^3.0.0, postcss-load-config@^3.1.4:
lilconfig "^2.0.5"
yaml "^1.10.2"
postcss-load-config@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd"
integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==
dependencies:
lilconfig "^2.0.5"
yaml "^2.1.1"
postcss-loader@^6.1.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef"
@ -20930,12 +20972,12 @@ postcss-modules@^4.0.0:
postcss-modules-values "^4.0.0"
string-hash "^1.1.1"
postcss-nested@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735"
integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==
postcss-nested@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c"
integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
dependencies:
postcss-selector-parser "^6.0.10"
postcss-selector-parser "^6.0.11"
postcss-normalize-charset@^5.1.0:
version "5.1.0"
@ -21108,14 +21150,6 @@ postcss-reduce-transforms@^6.0.0:
dependencies:
postcss-value-parser "^4.2.0"
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9:
version "6.0.13"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b"
@ -21124,6 +21158,14 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.5, postcss-selecto
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-svgo@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d"
@ -21182,7 +21224,7 @@ postcss@8.4.21:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.0.9, postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.24:
postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.24:
version "8.4.24"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.24.tgz#f714dba9b2284be3cc07dbd2fc57ee4dc972d2df"
integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==
@ -21191,6 +21233,15 @@ postcss@^8.0.9, postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.24:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.23:
version "8.4.29"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd"
integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
preact@10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.4.1.tgz#9b3ba020547673a231c6cf16f0fbaef0e8863431"
@ -22360,6 +22411,15 @@ resolve@^1.17.0:
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@^1.22.2:
version "1.22.6"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@^2.0.0-next.3, resolve@^2.0.0-next.4:
version "2.0.0-next.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"
@ -23702,6 +23762,19 @@ stylus@^0.59.0:
sax "~1.2.4"
source-map "^0.7.3"
sucrase@^3.32.0:
version "3.34.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f"
integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.2"
commander "^4.0.0"
glob "7.1.6"
lines-and-columns "^1.1.6"
mz "^2.7.0"
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
superstruct@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b"
@ -23827,34 +23900,33 @@ table@6.8.0:
string-width "^4.2.3"
strip-ansi "^6.0.1"
tailwindcss@3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.7.tgz#5936dd08c250b05180f0944500c01dce19188c07"
integrity sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==
tailwindcss@3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
color-name "^1.1.4"
detective "^5.2.1"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
glob-parent "^6.0.2"
is-glob "^4.0.3"
lilconfig "^2.0.6"
jiti "^1.18.2"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.0.9"
postcss-import "^14.1.0"
postcss-js "^4.0.0"
postcss-load-config "^3.1.4"
postcss-nested "6.0.0"
postcss "^8.4.23"
postcss-import "^15.1.0"
postcss-js "^4.0.1"
postcss-load-config "^4.0.1"
postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.11"
postcss-value-parser "^4.2.0"
quick-lru "^5.1.1"
resolve "^1.22.1"
resolve "^1.22.2"
sucrase "^3.32.0"
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
version "2.2.1"
@ -24017,6 +24089,20 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.1"
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
dependencies:
any-promise "^1.0.0"
thread-stream@^0.15.1:
version "0.15.2"
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.15.2.tgz#fb95ad87d2f1e28f07116eb23d85aba3bc0425f4"
@ -24181,6 +24267,11 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0:
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
ts-invariant@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c"
@ -25399,7 +25490,7 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
@ -25439,6 +25530,11 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.1.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144"
integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==
yargs-parser@20.2.4:
version "20.2.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"