Compare commits
15 Commits
feat/5702-
...
develop
Author | SHA1 | Date | |
---|---|---|---|
88264d890d | |||
|
26b78f878b | ||
|
054c0377b4 | ||
|
29bcbd06fb | ||
|
1d721dc748 | ||
|
1d71a839b3 | ||
|
4b917926c5 | ||
|
3a56678403 | ||
|
bcb5351dfc | ||
|
177e72dd16 | ||
|
8221882346 | ||
|
b1a8473131 | ||
|
f18216f70f | ||
|
de2b79416e | ||
|
6504912284 |
@ -4,6 +4,5 @@ tmp/*
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
dockerfiles
|
dockerfiles
|
||||||
node_modules
|
node_modules
|
||||||
.git
|
|
||||||
.github
|
.github
|
||||||
.vscode
|
.vscode
|
||||||
|
5
.github/workflows/ci-cd-trigger.yml
vendored
5
.github/workflows/ci-cd-trigger.yml
vendored
@ -196,9 +196,9 @@ jobs:
|
|||||||
cypress:
|
cypress:
|
||||||
needs: [build-sources, check-e2e-needed]
|
needs: [build-sources, check-e2e-needed]
|
||||||
name: '(CI) cypress'
|
name: '(CI) cypress'
|
||||||
if: ${{ needs.check-e2e-needed.outputs.run-tests == 'true' }}
|
|
||||||
uses: ./.github/workflows/cypress-run.yml
|
uses: ./.github/workflows/cypress-run.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
if: needs.check-e2e-needed.outputs.run-tests == 'true' && (contains(needs.build-sources.outputs.projects, 'governance') || contains(needs.build-sources.outputs.projects, 'explorer'))
|
||||||
with:
|
with:
|
||||||
projects: ${{ needs.build-sources.outputs.projects-e2e }}
|
projects: ${{ needs.build-sources.outputs.projects-e2e }}
|
||||||
tags: '@smoke'
|
tags: '@smoke'
|
||||||
@ -287,8 +287,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- run: |
|
- run: |
|
||||||
result="${{ needs.cypress.result }}"
|
result="${{ needs.cypress.result }}"
|
||||||
|
echo "Result: $result"
|
||||||
if [[ $result == "success" || $result == "skipped" ]]; then
|
if [[ $result == "success" || $result == "skipped" ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
exit 0
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
28
.verdaccio/config.yml
Normal file
28
.verdaccio/config.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# path to a directory with all packages
|
||||||
|
storage: ../tmp/local-registry/storage
|
||||||
|
|
||||||
|
# a list of other known repositories we can talk to
|
||||||
|
uplinks:
|
||||||
|
npmjs:
|
||||||
|
url: https://registry.yarnpkg.com
|
||||||
|
maxage: 60m
|
||||||
|
|
||||||
|
packages:
|
||||||
|
'**':
|
||||||
|
# give all users (including non-authenticated users) full access
|
||||||
|
# because it is a local registry
|
||||||
|
access: $all
|
||||||
|
publish: $all
|
||||||
|
unpublish: $all
|
||||||
|
|
||||||
|
# if package is not available locally, proxy requests to npm registry
|
||||||
|
proxy: npmjs
|
||||||
|
|
||||||
|
# log settings
|
||||||
|
logs:
|
||||||
|
type: stdout
|
||||||
|
format: pretty
|
||||||
|
level: warn
|
||||||
|
|
||||||
|
publish:
|
||||||
|
allow_offline: true # set offline to true to allow publish offline
|
@ -1,7 +1,9 @@
|
|||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||||
const theme = require('../../libs/tailwindcss-config/src/theme');
|
const { theme } = require('../../libs/tailwindcss-config/src/theme');
|
||||||
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
const {
|
||||||
|
vegaCustomClasses,
|
||||||
|
} = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
|
@ -153,7 +153,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
|||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('not.eq', currentBlockHeight);
|
.should('not.eq', currentBlockHeight);
|
||||||
});
|
});
|
||||||
cy.getByTestId('subscription-cell').should('have.text', 'Yes');
|
cy.getByTestId('subscription-cell').should('be.be.visible');
|
||||||
});
|
});
|
||||||
cy.getByTestId('connect').should('be.disabled');
|
cy.getByTestId('connect').should('be.disabled');
|
||||||
cy.getByTestId('node-url-custom').click({ force: true });
|
cy.getByTestId('node-url-custom').click({ force: true });
|
||||||
|
@ -26,9 +26,9 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
|||||||
const { token, staking, vesting } = useContracts();
|
const { token, staking, vesting } = useContracts();
|
||||||
const setAssociatedBalances = useRefreshAssociatedBalances();
|
const setAssociatedBalances = useRefreshAssociatedBalances();
|
||||||
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
|
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
|
||||||
const vegaConnecting = useEagerConnect();
|
const vegaWalletStatus = useEagerConnect();
|
||||||
|
|
||||||
const loaded = balancesLoaded && !vegaConnecting;
|
const loaded = balancesLoaded && vegaWalletStatus !== 'connecting';
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
@ -169,3 +169,5 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
|||||||
}
|
}
|
||||||
return <Suspense fallback={loading}>{children}</Suspense>;
|
return <Suspense fallback={loading}>{children}</Suspense>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AppLoader.displayName = 'AppLoader';
|
||||||
|
@ -111,3 +111,4 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
|
|||||||
</ContractsContext.Provider>
|
</ContractsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
ContractsProvider.displayName = 'ContractsProvider';
|
||||||
|
@ -38,6 +38,7 @@ import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
|
|||||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||||
import { MarketName } from '../proposal/market-name';
|
import { MarketName } from '../proposal/market-name';
|
||||||
import { Indicator } from '../proposal/indicator';
|
import { Indicator } from '../proposal/indicator';
|
||||||
|
import { type ProposalNode } from '../proposal/proposal-utils';
|
||||||
|
|
||||||
const ProposalTypeTags = ({
|
const ProposalTypeTags = ({
|
||||||
proposal,
|
proposal,
|
||||||
@ -540,10 +541,12 @@ const BatchProposalStateText = ({
|
|||||||
|
|
||||||
export const ProposalHeader = ({
|
export const ProposalHeader = ({
|
||||||
proposal,
|
proposal,
|
||||||
|
restData,
|
||||||
isListItem = true,
|
isListItem = true,
|
||||||
voteState,
|
voteState,
|
||||||
}: {
|
}: {
|
||||||
proposal: Proposal | BatchProposal;
|
proposal: Proposal | BatchProposal;
|
||||||
|
restData?: ProposalNode | null;
|
||||||
isListItem?: boolean;
|
isListItem?: boolean;
|
||||||
voteState?: VoteState | null;
|
voteState?: VoteState | null;
|
||||||
}) => {
|
}) => {
|
||||||
@ -595,7 +598,7 @@ export const ProposalHeader = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ProposalDetails proposal={proposal} />
|
<ProposalDetails proposal={proposal} />
|
||||||
<VoteBreakdown proposal={proposal} />
|
<VoteBreakdown proposal={proposal} restData={restData} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -91,6 +91,28 @@ export type ProposalNode = {
|
|||||||
proposal: ProposalData;
|
proposal: ProposalData;
|
||||||
proposalType: ProposalNodeType;
|
proposalType: ProposalNodeType;
|
||||||
proposals: SubProposalData[];
|
proposals: SubProposalData[];
|
||||||
|
yes?: [
|
||||||
|
{
|
||||||
|
partyId: string;
|
||||||
|
elsPerMarket?: [
|
||||||
|
{
|
||||||
|
marketId: string;
|
||||||
|
els: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
no?: [
|
||||||
|
{
|
||||||
|
partyId: string;
|
||||||
|
elsPerMarket?: [
|
||||||
|
{
|
||||||
|
marketId: string;
|
||||||
|
els: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
type SingleProposalNode = ProposalNode & {
|
type SingleProposalNode = ProposalNode & {
|
||||||
|
@ -48,6 +48,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
|
|||||||
|
|
||||||
<ProposalHeader
|
<ProposalHeader
|
||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
|
restData={restData}
|
||||||
isListItem={false}
|
isListItem={false}
|
||||||
voteState={voteState}
|
voteState={voteState}
|
||||||
/>
|
/>
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
|
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
|
||||||
import { MarketName } from '../proposal/market-name';
|
import { MarketName } from '../proposal/market-name';
|
||||||
import { Indicator } from '../proposal/indicator';
|
import { Indicator } from '../proposal/indicator';
|
||||||
|
import { type ProposalNode } from '../proposal/proposal-utils';
|
||||||
|
|
||||||
export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
||||||
<CompactNumber
|
<CompactNumber
|
||||||
@ -110,24 +111,64 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
|
|||||||
|
|
||||||
export const VoteBreakdown = ({
|
export const VoteBreakdown = ({
|
||||||
proposal,
|
proposal,
|
||||||
|
restData,
|
||||||
}: {
|
}: {
|
||||||
proposal: Proposal | BatchProposal;
|
proposal: Proposal | BatchProposal;
|
||||||
|
restData?: ProposalNode | null;
|
||||||
}) => {
|
}) => {
|
||||||
if (proposal.__typename === 'Proposal') {
|
if (proposal.__typename === 'Proposal') {
|
||||||
return <VoteBreakdownNormal proposal={proposal} />;
|
return <VoteBreakdownNormal proposal={proposal} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proposal.__typename === 'BatchProposal') {
|
if (proposal.__typename === 'BatchProposal') {
|
||||||
return <VoteBreakdownBatch proposal={proposal} />;
|
return <VoteBreakdownBatch proposal={proposal} restData={restData} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
|
const VoteBreakdownBatch = ({
|
||||||
|
proposal,
|
||||||
|
restData,
|
||||||
|
}: {
|
||||||
|
proposal: BatchProposal;
|
||||||
|
restData?: ProposalNode | null;
|
||||||
|
}) => {
|
||||||
const [fullBreakdown, setFullBreakdown] = useState(false);
|
const [fullBreakdown, setFullBreakdown] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const yesELS =
|
||||||
|
restData?.yes?.reduce((all, y) => {
|
||||||
|
if (y.elsPerMarket) {
|
||||||
|
y.elsPerMarket.forEach((item) => {
|
||||||
|
const share = Number(item.els);
|
||||||
|
if (all[item.marketId]) {
|
||||||
|
all[item.marketId].push(share);
|
||||||
|
} else {
|
||||||
|
all[item.marketId] = [share];
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}, {} as Record<string, number[]>) || {};
|
||||||
|
|
||||||
|
const noELS =
|
||||||
|
restData?.no?.reduce((all, y) => {
|
||||||
|
if (y.elsPerMarket) {
|
||||||
|
y.elsPerMarket.forEach((item) => {
|
||||||
|
const share = Number(item.els);
|
||||||
|
if (all[item.marketId]) {
|
||||||
|
all[item.marketId].push(share);
|
||||||
|
} else {
|
||||||
|
all[item.marketId] = [share];
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return all;
|
||||||
|
}, {} as Record<string, number[]>) || {};
|
||||||
|
|
||||||
const voteInfo = useBatchVoteInformation({
|
const voteInfo = useBatchVoteInformation({
|
||||||
terms: compact(
|
terms: compact(
|
||||||
proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : []
|
proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : []
|
||||||
@ -194,6 +235,8 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
|
|||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
votes={proposal.votes}
|
votes={proposal.votes}
|
||||||
terms={p.terms}
|
terms={p.terms}
|
||||||
|
yesELS={yesELS}
|
||||||
|
noELS={noELS}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -254,6 +297,8 @@ const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
|
|||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
votes={proposal.votes}
|
votes={proposal.votes}
|
||||||
terms={p.terms}
|
terms={p.terms}
|
||||||
|
yesELS={yesELS}
|
||||||
|
noELS={noELS}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -271,17 +316,17 @@ const VoteBreakdownBatchSubProposal = ({
|
|||||||
votes,
|
votes,
|
||||||
terms,
|
terms,
|
||||||
indicator,
|
indicator,
|
||||||
|
yesELS,
|
||||||
|
noELS,
|
||||||
}: {
|
}: {
|
||||||
proposal: BatchProposal;
|
proposal: BatchProposal;
|
||||||
votes: VoteFieldsFragment;
|
votes: VoteFieldsFragment;
|
||||||
terms: ProposalTermsFieldsFragment;
|
terms: ProposalTermsFieldsFragment;
|
||||||
indicator?: number;
|
indicator?: number;
|
||||||
|
yesELS: Record<string, number[]>;
|
||||||
|
noELS: Record<string, number[]>;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const voteInfo = useVoteInformation({
|
|
||||||
votes,
|
|
||||||
terms,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||||
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
|
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
|
||||||
@ -294,6 +339,15 @@ const VoteBreakdownBatchSubProposal = ({
|
|||||||
marketId = terms.change.market.id;
|
marketId = terms.change.market.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const voteInfo = useVoteInformation({
|
||||||
|
votes,
|
||||||
|
terms,
|
||||||
|
// yes votes ELS for this specific proposal (market)
|
||||||
|
yesELS: marketId ? yesELS[marketId] : undefined,
|
||||||
|
// no votes ELS for this specific proposal (market)
|
||||||
|
noELS: marketId ? noELS[marketId] : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const marketName = marketId ? (
|
const marketName = marketId ? (
|
||||||
<>
|
<>
|
||||||
: <MarketName marketId={marketId} />
|
: <MarketName marketId={marketId} />
|
||||||
|
@ -8,13 +8,18 @@ import {
|
|||||||
type VoteFieldsFragment,
|
type VoteFieldsFragment,
|
||||||
} from '../__generated__/Proposals';
|
} from '../__generated__/Proposals';
|
||||||
import { type ProposalChangeType } from '../types';
|
import { type ProposalChangeType } from '../types';
|
||||||
|
import sum from 'lodash/sum';
|
||||||
|
|
||||||
export const useVoteInformation = ({
|
export const useVoteInformation = ({
|
||||||
votes,
|
votes,
|
||||||
terms,
|
terms,
|
||||||
|
yesELS,
|
||||||
|
noELS,
|
||||||
}: {
|
}: {
|
||||||
votes: VoteFieldsFragment;
|
votes: VoteFieldsFragment;
|
||||||
terms: ProposalTermsFieldsFragment;
|
terms: ProposalTermsFieldsFragment;
|
||||||
|
yesELS?: number[];
|
||||||
|
noELS?: number[];
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
appState: { totalSupply, decimals },
|
appState: { totalSupply, decimals },
|
||||||
@ -31,7 +36,9 @@ export const useVoteInformation = ({
|
|||||||
paramsForChange,
|
paramsForChange,
|
||||||
votes,
|
votes,
|
||||||
totalSupply,
|
totalSupply,
|
||||||
decimals
|
decimals,
|
||||||
|
yesELS,
|
||||||
|
noELS
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,7 +79,11 @@ const getVoteData = (
|
|||||||
},
|
},
|
||||||
votes: ProposalFieldsFragment['votes'],
|
votes: ProposalFieldsFragment['votes'],
|
||||||
totalSupply: BigNumber,
|
totalSupply: BigNumber,
|
||||||
decimals: number
|
decimals: number,
|
||||||
|
/** A list of ELS yes votes */
|
||||||
|
yesELS?: number[],
|
||||||
|
/** A list if ELS no votes */
|
||||||
|
noELS?: number[]
|
||||||
) => {
|
) => {
|
||||||
const requiredMajorityPercentage = params.requiredMajority
|
const requiredMajorityPercentage = params.requiredMajority
|
||||||
? new BigNumber(params.requiredMajority).times(100)
|
? new BigNumber(params.requiredMajority).times(100)
|
||||||
@ -86,17 +97,31 @@ const getVoteData = (
|
|||||||
addDecimal(votes.no.totalTokens ?? 0, decimals)
|
addDecimal(votes.no.totalTokens ?? 0, decimals)
|
||||||
);
|
);
|
||||||
|
|
||||||
const noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
|
let noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
|
||||||
? new BigNumber(0)
|
? new BigNumber(0)
|
||||||
: new BigNumber(votes.no.totalEquityLikeShareWeight).times(100);
|
: new BigNumber(votes.no.totalEquityLikeShareWeight).times(100);
|
||||||
|
// there's no meaningful `totalEquityLikeShareWeight` in batch proposals,
|
||||||
|
// it has to be deduced from `elsPerMarket` of `no` votes of given proposal
|
||||||
|
// data. (by REST DATA)
|
||||||
|
if (noELS != null) {
|
||||||
|
const noTotalELS = sum(noELS);
|
||||||
|
noEquityLikeShareWeight = new BigNumber(noTotalELS).times(100);
|
||||||
|
}
|
||||||
|
|
||||||
const yesTokens = new BigNumber(
|
const yesTokens = new BigNumber(
|
||||||
addDecimal(votes.yes.totalTokens ?? 0, decimals)
|
addDecimal(votes.yes.totalTokens ?? 0, decimals)
|
||||||
);
|
);
|
||||||
|
|
||||||
const yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
|
let yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
|
||||||
? new BigNumber(0)
|
? new BigNumber(0)
|
||||||
: new BigNumber(votes.yes.totalEquityLikeShareWeight).times(100);
|
: new BigNumber(votes.yes.totalEquityLikeShareWeight).times(100);
|
||||||
|
// there's no meaningful `totalEquityLikeShareWeight` in batch proposals,
|
||||||
|
// it has to be deduced from `elsPerMarket` of `yes` votes of given proposal
|
||||||
|
// data. (by REST DATA)
|
||||||
|
if (noELS != null) {
|
||||||
|
const yesTotalELS = sum(yesELS);
|
||||||
|
yesEquityLikeShareWeight = new BigNumber(yesTotalELS).times(100);
|
||||||
|
}
|
||||||
|
|
||||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
const totalTokensVoted = yesTokens.plus(noTokens);
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||||
const theme = require('../../libs/tailwindcss-config/src/theme');
|
const { theme } = require('../../libs/tailwindcss-config/src/theme');
|
||||||
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
const {
|
||||||
|
vegaCustomClasses,
|
||||||
|
} = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
||||||
|
|
||||||
module.exports = {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
join(__dirname, 'src/**/*.{js,ts,jsx,tsx}'),
|
join(__dirname, 'src/**/*.{js,ts,jsx,tsx}'),
|
||||||
'libs/ui-toolkit/src/utils/shared.ts',
|
'libs/ui-toolkit/src/utils/shared.ts',
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"@nx/react/babel",
|
|
||||||
{
|
|
||||||
"runtime": "automatic"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
# This file is used by:
|
|
||||||
# 1. autoprefixer to adjust CSS to support the below specified browsers
|
|
||||||
# 2. babel preset-env to adjust included polyfills
|
|
||||||
#
|
|
||||||
# For additional information regarding the format and rule options, please see:
|
|
||||||
# https://github.com/browserslist/browserslist#queries
|
|
||||||
#
|
|
||||||
# If you need to support different browsers in production, you may tweak the list below.
|
|
||||||
|
|
||||||
last 1 Chrome version
|
|
||||||
last 1 Firefox version
|
|
||||||
last 2 Edge major versions
|
|
||||||
last 2 Safari major version
|
|
||||||
last 2 iOS major versions
|
|
||||||
Firefox ESR
|
|
||||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
|
@ -1,28 +0,0 @@
|
|||||||
# React Environment Variables
|
|
||||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
|
||||||
|
|
||||||
# Netlify Environment Variables
|
|
||||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
|
||||||
NX_VERSION=\$npm_package_version
|
|
||||||
NX_REPOSITORY_URL=\$REPOSITORY_URL
|
|
||||||
NX_BRANCH=\$BRANCH
|
|
||||||
NX_PULL_REQUEST=\$PULL_REQUEST
|
|
||||||
NX_HEAD=\$HEAD
|
|
||||||
NX_COMMIT_REF=\$COMMIT_REF
|
|
||||||
NX_CONTEXT=\$CONTEXT
|
|
||||||
NX_REVIEW_ID=\$REVIEW_ID
|
|
||||||
NX_INCOMING_HOOK_TITLE=\$INCOMING_HOOK_TITLE
|
|
||||||
NX_INCOMING_HOOK_URL=\$INCOMING_HOOK_URL
|
|
||||||
NX_INCOMING_HOOK_BODY=\$INCOMING_HOOK_BODY
|
|
||||||
NX_URL=\$URL
|
|
||||||
NX_DEPLOY_URL=\$DEPLOY_URL
|
|
||||||
NX_DEPLOY_PRIME_URL=\$DEPLOY_PRIME_URL
|
|
||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
|
|
||||||
NX_VEGA_ENV = 'TESTNET'
|
|
||||||
NX_VEGA_URL="https://api.n07.testnet.vega.xyz/graphql"
|
|
||||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
|
||||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
|
|
||||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
|
||||||
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
|
@ -1,3 +0,0 @@
|
|||||||
# App configuration variables
|
|
||||||
NX_VEGA_URL=http://localhost:3008/graphql
|
|
||||||
NX_VEGA_ENV=LOCAL
|
|
@ -1,8 +0,0 @@
|
|||||||
# App configuration variables
|
|
||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
|
|
||||||
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
|
|
||||||
NX_VEGA_ENV=DEVNET
|
|
||||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
|
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
|
||||||
NX_VEGA_EXPLORER_URL=#
|
|
@ -1,9 +0,0 @@
|
|||||||
# App configuration variables
|
|
||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
|
|
||||||
NX_VEGA_URL=https://api.vega.community/graphql
|
|
||||||
NX_VEGA_ENV=MAINNET
|
|
||||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\"}
|
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
|
||||||
NX_ETHERSCAN_URL=https://etherscan.io
|
|
||||||
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
|
|
||||||
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
|
|
@ -1,9 +0,0 @@
|
|||||||
# App configuration variables
|
|
||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
|
||||||
NX_VEGA_URL=https://api.n00.stagnet1.vega.xyz/graphql
|
|
||||||
NX_VEGA_ENV=STAGNET1
|
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
|
||||||
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
|
|
||||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
|||||||
# App configuration variables
|
|
||||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
|
|
||||||
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
|
||||||
NX_VEGA_ENV=TESTNET
|
|
||||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\"}
|
|
||||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
|
||||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
|
||||||
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
|
||||||
"ignorePatterns": ["!**/*", "__generated__"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
export default {
|
|
||||||
displayName: 'liquidity-provision-dashboard',
|
|
||||||
preset: '../../jest.preset.js',
|
|
||||||
transform: {
|
|
||||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
|
||||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
|
||||||
},
|
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
||||||
coverageDirectory: '../../coverage/apps/liquidity-provision-dashboard',
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
const { join } = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {
|
|
||||||
config: join(__dirname, 'tailwind.config.js'),
|
|
||||||
},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,94 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "liquidity-provision-dashboard",
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"sourceRoot": "apps/liquidity-provision-dashboard/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"targets": {
|
|
||||||
"build": {
|
|
||||||
"executor": "@nx/webpack:webpack",
|
|
||||||
"outputs": ["{options.outputPath}"],
|
|
||||||
"defaultConfiguration": "production",
|
|
||||||
"options": {
|
|
||||||
"compiler": "babel",
|
|
||||||
"outputPath": "dist/apps/liquidity-provision-dashboard",
|
|
||||||
"index": "apps/liquidity-provision-dashboard/src/index.html",
|
|
||||||
"baseHref": "/",
|
|
||||||
"main": "apps/liquidity-provision-dashboard/src/main.tsx",
|
|
||||||
"polyfills": "apps/liquidity-provision-dashboard/src/polyfills.ts",
|
|
||||||
"tsConfig": "apps/liquidity-provision-dashboard/tsconfig.app.json",
|
|
||||||
"assets": [
|
|
||||||
"apps/liquidity-provision-dashboard/src/favicon.ico",
|
|
||||||
"apps/liquidity-provision-dashboard/src/assets"
|
|
||||||
],
|
|
||||||
"styles": ["apps/liquidity-provision-dashboard/src/styles.scss"],
|
|
||||||
"scripts": [],
|
|
||||||
"webpackConfig": "@nx/react/plugins/webpack"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"development": {
|
|
||||||
"extractLicenses": false,
|
|
||||||
"optimization": false,
|
|
||||||
"sourceMap": true,
|
|
||||||
"vendorChunk": true
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "apps/liquidity-provision-dashboard/src/environments/environment.ts",
|
|
||||||
"with": "apps/liquidity-provision-dashboard/src/environments/environment.prod.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"optimization": true,
|
|
||||||
"outputHashing": "all",
|
|
||||||
"sourceMap": false,
|
|
||||||
"namedChunks": false,
|
|
||||||
"extractLicenses": true,
|
|
||||||
"vendorChunk": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"serve": {
|
|
||||||
"executor": "@nx/webpack:dev-server",
|
|
||||||
"options": {
|
|
||||||
"buildTarget": "liquidity-provision-dashboard:build",
|
|
||||||
"hmr": true,
|
|
||||||
"port": 4201
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"development": {
|
|
||||||
"buildTarget": "liquidity-provision-dashboard:build:development"
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"buildTarget": "liquidity-provision-dashboard:build:production",
|
|
||||||
"hmr": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"executor": "@nx/eslint:lint",
|
|
||||||
"outputs": ["{options.outputFile}"],
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": [
|
|
||||||
"apps/liquidity-provision-dashboard/**/*.{ts,tsx,js,jsx}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test": {
|
|
||||||
"executor": "@nx/jest:jest",
|
|
||||||
"outputs": [
|
|
||||||
"{workspaceRoot}/coverage/apps/liquidity-provision-dashboard"
|
|
||||||
],
|
|
||||||
"options": {
|
|
||||||
"jestConfig": "apps/liquidity-provision-dashboard/jest.config.ts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"build-spec": {
|
|
||||||
"executor": "nx:run-commands",
|
|
||||||
"outputs": [],
|
|
||||||
"options": {
|
|
||||||
"command": "yarn tsc --project ./apps/liquidity-provision-dashboard/tsconfig.spec.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
|
||||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
|
||||||
import { useRoutes } from 'react-router-dom';
|
|
||||||
|
|
||||||
import '../styles.scss';
|
|
||||||
import { Navbar } from './components/navbar';
|
|
||||||
|
|
||||||
import { routerConfig } from './routes/router-config';
|
|
||||||
|
|
||||||
const cache: InMemoryCacheConfig = {
|
|
||||||
typePolicies: {
|
|
||||||
Market: {
|
|
||||||
merge: true,
|
|
||||||
},
|
|
||||||
Party: {
|
|
||||||
merge: true,
|
|
||||||
},
|
|
||||||
Query: {},
|
|
||||||
Account: {
|
|
||||||
keyFields: false,
|
|
||||||
fields: {
|
|
||||||
balanceFormatted: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Node: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
Instrument: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const AppRouter = () => useRoutes(routerConfig);
|
|
||||||
|
|
||||||
export function App() {
|
|
||||||
useInitializeEnv();
|
|
||||||
return (
|
|
||||||
<NetworkLoader cache={cache}>
|
|
||||||
<div className="max-h-full min-h-full bg-white">
|
|
||||||
<Navbar />
|
|
||||||
<AppRouter />
|
|
||||||
</div>
|
|
||||||
</NetworkLoader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
@ -1,25 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
|
|
||||||
import { Intro } from './intro';
|
|
||||||
import { MarketList } from './market-list';
|
|
||||||
|
|
||||||
export function Dashboard() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="px-16 pt-20 pb-12 bg-greys-light-100">
|
|
||||||
<div className="max-w-screen-xl mx-auto">
|
|
||||||
<h1 className="font-alpha calt uppercase text-5xl mb-8">
|
|
||||||
{t('Top liquidity opportunities')}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<Intro />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-16 py-6">
|
|
||||||
<div className="max-w-screen-xl mx-auto">
|
|
||||||
<MarketList />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './dashboard';
|
|
@ -1 +0,0 @@
|
|||||||
export * from './intro';
|
|
@ -1,53 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
// TODO: add mainnet links once docs have been updated
|
|
||||||
const LINKS = {
|
|
||||||
testnet: [
|
|
||||||
{
|
|
||||||
label: 'Learn about liquidity fees',
|
|
||||||
url: 'https://docs.vega.xyz/testnet/tutorials/providing-liquidity#resources',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Provide liquidity',
|
|
||||||
url: 'https://docs.vega.xyz/testnet/tutorials/providing-liquidity#overview',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'View your liquidity provisions',
|
|
||||||
url: 'https://docs.vega.xyz/testnet/tutorials/providing-liquidity#viewing-existing-liquidity-provisions',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Amend or remove liquidity',
|
|
||||||
url: 'https://docs.vega.xyz/testnet/tutorials/providing-liquidity#amending-a-liquidity-commitment',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
mainnet: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: update this when network switcher is added
|
|
||||||
type Network = 'testnet' | 'mainnet';
|
|
||||||
|
|
||||||
export const Intro = ({ network = 'testnet' }: { network?: Network }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p className="font-alpha calt text-2xl font-medium mb-2">
|
|
||||||
{t(
|
|
||||||
'Become a liquidity provider and earn a cut of the fees paid during trading.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<ul className="flex flex-wrap">
|
|
||||||
{LINKS[network].map(
|
|
||||||
({ label, url }: { label: string; url: string }) => (
|
|
||||||
<li key={url} className="mr-6">
|
|
||||||
<ExternalLink href={url} rel="noreferrer">
|
|
||||||
{t(label)}
|
|
||||||
</ExternalLink>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './market-list';
|
|
@ -1,296 +0,0 @@
|
|||||||
import { DApp, useLinks } from '@vegaprotocol/environment';
|
|
||||||
import { type Market } from '@vegaprotocol/liquidity';
|
|
||||||
import {
|
|
||||||
displayChange,
|
|
||||||
formatWithAsset,
|
|
||||||
useMarketsLiquidity,
|
|
||||||
} from '@vegaprotocol/liquidity';
|
|
||||||
import {
|
|
||||||
addDecimalsFormatNumber,
|
|
||||||
formatNumberPercentage,
|
|
||||||
getExpiryDate,
|
|
||||||
toBigNum,
|
|
||||||
} from '@vegaprotocol/utils';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { type VegaValueFormatterParams } from '@vegaprotocol/datagrid';
|
|
||||||
import { PriceChangeCell } from '@vegaprotocol/datagrid';
|
|
||||||
import type * as Schema from '@vegaprotocol/types';
|
|
||||||
import {
|
|
||||||
AsyncRenderer,
|
|
||||||
Icon,
|
|
||||||
HealthBar,
|
|
||||||
TooltipCellComponent,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import {
|
|
||||||
type GetRowIdParams,
|
|
||||||
type RowClickedEvent,
|
|
||||||
type ColDef,
|
|
||||||
} from 'ag-grid-community';
|
|
||||||
import 'ag-grid-community/styles/ag-grid.css';
|
|
||||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
|
||||||
import { useCallback, useState, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { Grid } from '../../grid';
|
|
||||||
import { HealthDialog } from '../../health-dialog';
|
|
||||||
import { Status } from '../../status';
|
|
||||||
import { intentForStatus } from '../../../lib/utils';
|
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
|
||||||
import { getAsset } from '@vegaprotocol/markets';
|
|
||||||
|
|
||||||
export const MarketList = () => {
|
|
||||||
const { data, error, loading } = useMarketsLiquidity();
|
|
||||||
const [isHealthDialogOpen, setIsHealthDialogOpen] = useState(false);
|
|
||||||
const consoleLink = useLinks(DApp.Console);
|
|
||||||
|
|
||||||
const getRowId = useCallback(({ data }: GetRowIdParams) => data.id, []);
|
|
||||||
const columnDefs = useMemo<ColDef[]>(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
headerName: t('Market (futures)'),
|
|
||||||
field: 'tradableInstrument.instrument.name',
|
|
||||||
cellRenderer: ({ value, data }: { value: string; data: Market }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className="leading-3">{value}</span>
|
|
||||||
<span className="leading-3">{getAsset(data).symbol}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
minWidth: 100,
|
|
||||||
flex: 1,
|
|
||||||
headerTooltip: t('The market name and settlement asset'),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Market Code'),
|
|
||||||
headerTooltip: t(
|
|
||||||
'The market code is a unique identifier for this market'
|
|
||||||
),
|
|
||||||
field: 'tradableInstrument.instrument.code',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Type'),
|
|
||||||
headerTooltip: t('Type'),
|
|
||||||
field: 'tradableInstrument.instrument.product.__typename',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Last Price'),
|
|
||||||
headerTooltip: t('Latest price for this market'),
|
|
||||||
field: 'data.markPrice',
|
|
||||||
valueFormatter: ({
|
|
||||||
value,
|
|
||||||
data,
|
|
||||||
}: VegaValueFormatterParams<Market, 'data.markPrice'>) =>
|
|
||||||
value && data ? formatWithAsset(value, getAsset(data)) : '-',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Change (24h)'),
|
|
||||||
headerTooltip: t('Change in price over the last 24h'),
|
|
||||||
cellRenderer: ({
|
|
||||||
data,
|
|
||||||
}: VegaValueFormatterParams<Market, 'data.candles'>) => {
|
|
||||||
if (data && data.candles) {
|
|
||||||
const prices = data.candles.map((candle) => candle.close);
|
|
||||||
return (
|
|
||||||
<PriceChangeCell
|
|
||||||
candles={prices}
|
|
||||||
decimalPlaces={data?.decimalPlaces}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else return <div>{t('-')}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Volume (24h)'),
|
|
||||||
field: 'dayVolume',
|
|
||||||
valueFormatter: ({
|
|
||||||
value,
|
|
||||||
data,
|
|
||||||
}: VegaValueFormatterParams<Market, 'dayVolume'>) =>
|
|
||||||
value && data
|
|
||||||
? `${addDecimalsFormatNumber(
|
|
||||||
value,
|
|
||||||
getAsset(data).decimals || 0
|
|
||||||
)} (${displayChange(data.volumeChange)})`
|
|
||||||
: '-',
|
|
||||||
headerTooltip: t('The trade volume over the last 24h'),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Total staked by LPs'),
|
|
||||||
field: 'liquidityCommitted',
|
|
||||||
valueFormatter: ({
|
|
||||||
value,
|
|
||||||
data,
|
|
||||||
}: VegaValueFormatterParams<Market, 'liquidityCommitted'>) =>
|
|
||||||
data && value
|
|
||||||
? formatWithAsset(value.toString(), getAsset(data))
|
|
||||||
: '-',
|
|
||||||
headerTooltip: t('The amount of funds allocated to provide liquidity'),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Target stake'),
|
|
||||||
field: 'target',
|
|
||||||
valueFormatter: ({
|
|
||||||
value,
|
|
||||||
data,
|
|
||||||
}: VegaValueFormatterParams<Market, 'target'>) =>
|
|
||||||
data && value ? formatWithAsset(value, getAsset(data)) : '-',
|
|
||||||
headerTooltip: t(
|
|
||||||
'The ideal committed liquidity to operate the market. If total commitment currently below this level then LPs can set the fee level with new commitment.'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('% Target stake met'),
|
|
||||||
valueFormatter: ({ data }: VegaValueFormatterParams<Market, ''>) => {
|
|
||||||
if (data) {
|
|
||||||
const roundedPercentage =
|
|
||||||
parseInt(
|
|
||||||
(data.liquidityCommitted / parseFloat(data.target)).toFixed(0)
|
|
||||||
) * 100;
|
|
||||||
const display = Number.isNaN(roundedPercentage)
|
|
||||||
? 'N/A'
|
|
||||||
: formatNumberPercentage(toBigNum(roundedPercentage, 0), 0);
|
|
||||||
return display;
|
|
||||||
} else return '-';
|
|
||||||
},
|
|
||||||
headerTooltip: t('% Target stake met'),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Fee levels'),
|
|
||||||
field: 'fees',
|
|
||||||
valueFormatter: ({ value }: VegaValueFormatterParams<Market, 'fees'>) =>
|
|
||||||
value ? `${value.factors.liquidityFee}%` : '-',
|
|
||||||
headerTooltip: t('Fee level for this market'),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('Status'),
|
|
||||||
field: 'tradingMode',
|
|
||||||
cellRenderer: ({
|
|
||||||
value,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
value: Schema.MarketTradingMode;
|
|
||||||
data: Market;
|
|
||||||
}) => {
|
|
||||||
return <Status trigger={data.data?.trigger} tradingMode={value} />;
|
|
||||||
},
|
|
||||||
headerTooltip: t(
|
|
||||||
'The current market status - those below the target stake mark are most in need of liquidity'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerComponent: () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<span>{t('Health')}</span>{' '}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsHealthDialogOpen(true)}
|
|
||||||
aria-label={t('open tooltip')}
|
|
||||||
>
|
|
||||||
<Icon name="info-sign" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
field: 'tradingMode',
|
|
||||||
cellRenderer: ({
|
|
||||||
value,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
value: Schema.MarketTradingMode;
|
|
||||||
data: Market;
|
|
||||||
}) => (
|
|
||||||
<HealthBar
|
|
||||||
target={data.target}
|
|
||||||
decimals={getAsset(data).decimals || 0}
|
|
||||||
levels={data.feeLevels}
|
|
||||||
intent={intentForStatus(value)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
sortable: false,
|
|
||||||
cellStyle: { overflow: 'unset' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('Age'),
|
|
||||||
field: 'marketTimestamps.open',
|
|
||||||
headerTooltip: t('Age of the market'),
|
|
||||||
valueFormatter: ({
|
|
||||||
value,
|
|
||||||
}: VegaValueFormatterParams<Market, 'marketTimestamps.open'>) => {
|
|
||||||
return value ? formatDistanceToNow(new Date(value)) : '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('Closing Time'),
|
|
||||||
field: 'tradableInstrument.instrument.metadata.tags',
|
|
||||||
headerTooltip: t('Closing time of the market'),
|
|
||||||
valueFormatter: ({ data }: VegaValueFormatterParams<Market, ''>) => {
|
|
||||||
let expiry;
|
|
||||||
if (data?.tradableInstrument.instrument.metadata.tags) {
|
|
||||||
expiry = getExpiryDate(
|
|
||||||
data?.tradableInstrument.instrument.metadata.tags,
|
|
||||||
data?.marketTimestamps.close,
|
|
||||||
data?.state
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return expiry ? expiry : '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
|
||||||
<div
|
|
||||||
className="w-full grow"
|
|
||||||
style={{ minHeight: 500, overflow: 'hidden' }}
|
|
||||||
>
|
|
||||||
<Grid
|
|
||||||
gridOptions={{
|
|
||||||
onRowClicked: ({ data }: RowClickedEvent) => {
|
|
||||||
window.open(
|
|
||||||
liquidityDetailsConsoleLink(data.id, consoleLink),
|
|
||||||
'_blank',
|
|
||||||
'noopener,noreferrer'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
rowData={data}
|
|
||||||
defaultColDef={{
|
|
||||||
resizable: true,
|
|
||||||
sortable: true,
|
|
||||||
unSortIcon: true,
|
|
||||||
cellClass: ['flex', 'flex-col', 'justify-center'],
|
|
||||||
tooltipComponent: TooltipCellComponent,
|
|
||||||
}}
|
|
||||||
columnDefs={columnDefs}
|
|
||||||
getRowId={getRowId}
|
|
||||||
isRowClickable
|
|
||||||
tooltipShowDelay={500}
|
|
||||||
/>
|
|
||||||
<HealthDialog
|
|
||||||
isOpen={isHealthDialogOpen}
|
|
||||||
onChange={() => {
|
|
||||||
setIsHealthDialogOpen(!isHealthDialogOpen);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AsyncRenderer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const liquidityDetailsConsoleLink = (
|
|
||||||
marketId: string,
|
|
||||||
consoleLink: (url: string | undefined) => string
|
|
||||||
) => consoleLink(`/#/liquidity/${marketId}`);
|
|
@ -1,103 +0,0 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { makeDerivedDataProvider } from '@vegaprotocol/data-provider';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getFeeLevels,
|
|
||||||
sumLiquidityCommitted,
|
|
||||||
lpAggregatedDataProvider,
|
|
||||||
} from '@vegaprotocol/liquidity';
|
|
||||||
import { getAsset, marketWithDataProvider } from '@vegaprotocol/markets';
|
|
||||||
import type { MarketWithData } from '@vegaprotocol/markets';
|
|
||||||
|
|
||||||
import { Market } from './market';
|
|
||||||
import { Header } from './header';
|
|
||||||
import { LPProvidersGrid } from './providers';
|
|
||||||
|
|
||||||
const formatMarket = (market: MarketWithData) => {
|
|
||||||
return {
|
|
||||||
name: market?.tradableInstrument.instrument.name,
|
|
||||||
symbol: getAsset(market).symbol,
|
|
||||||
settlementAsset: getAsset(market),
|
|
||||||
targetStake: market?.data?.targetStake,
|
|
||||||
tradingMode: market?.data?.marketTradingMode,
|
|
||||||
trigger: market?.data?.trigger,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const lpDataProvider = makeDerivedDataProvider(
|
|
||||||
[marketWithDataProvider, lpAggregatedDataProvider],
|
|
||||||
([market, lpAggregatedData]) => ({
|
|
||||||
market: { ...formatMarket(market) },
|
|
||||||
liquidityProviders: lpAggregatedData || [],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const useMarketDetails = (marketId: string | undefined) => {
|
|
||||||
const { data, loading, error } = useDataProvider({
|
|
||||||
dataProvider: lpDataProvider,
|
|
||||||
skipUpdates: true,
|
|
||||||
variables: { marketId: marketId || '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const liquidityProviders = data?.liquidityProviders || [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
name: data?.market?.name,
|
|
||||||
symbol: data?.market?.symbol,
|
|
||||||
liquidityProviders: liquidityProviders,
|
|
||||||
feeLevels: getFeeLevels(liquidityProviders),
|
|
||||||
comittedLiquidity: sumLiquidityCommitted(liquidityProviders) || 0,
|
|
||||||
settlementAsset: data?.market?.settlementAsset || {},
|
|
||||||
targetStake: data?.market?.targetStake || '0',
|
|
||||||
tradingMode: data?.market.tradingMode,
|
|
||||||
},
|
|
||||||
error,
|
|
||||||
loading: loading,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type Params = { marketId: string };
|
|
||||||
|
|
||||||
export const Detail = () => {
|
|
||||||
const { marketId } = useParams<Params>();
|
|
||||||
const { data, loading, error } = useMarketDetails(marketId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
|
||||||
<div className="bg-greys-light-100 px-16 pb-12 pt-14">
|
|
||||||
<div className="mx-auto max-w-screen-xl">
|
|
||||||
<Header name={data.name} symbol={data.symbol} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-16">
|
|
||||||
<div className="mx-auto max-w-screen-xl">
|
|
||||||
<div className="py-12">
|
|
||||||
{marketId && (
|
|
||||||
<Market
|
|
||||||
marketId={marketId}
|
|
||||||
feeLevels={data.feeLevels}
|
|
||||||
comittedLiquidity={data.comittedLiquidity}
|
|
||||||
settlementAsset={data.settlementAsset}
|
|
||||||
targetStake={data.targetStake}
|
|
||||||
tradingMode={data.tradingMode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="font-alpha calt mb-4 text-2xl">
|
|
||||||
{t('Current Liquidity Provision')}
|
|
||||||
</h2>
|
|
||||||
<LPProvidersGrid
|
|
||||||
liquidityProviders={data.liquidityProviders}
|
|
||||||
settlementAsset={data.settlementAsset}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AsyncRenderer>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
export const Header = ({
|
|
||||||
name,
|
|
||||||
symbol,
|
|
||||||
}: {
|
|
||||||
name?: string;
|
|
||||||
symbol?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-6">
|
|
||||||
<Link to="/">
|
|
||||||
<Icon name="chevron-left" className="mr-2" />
|
|
||||||
<span className="underline font-alpha calt text-lg font-medium">
|
|
||||||
{t('Liquidity opportunities')}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<h1 className="font-alpha calt text-5xl mb-6">{name}</h1>
|
|
||||||
<p className="font-alpha calt text-4xl">{symbol}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './header';
|
|
@ -1 +0,0 @@
|
|||||||
export * from './detail';
|
|
@ -1 +0,0 @@
|
|||||||
export * from './last-24h-volume';
|
|
@ -1,108 +0,0 @@
|
|||||||
import { useState, useMemo, useRef, useCallback } from 'react';
|
|
||||||
import throttle from 'lodash/throttle';
|
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
|
||||||
import { useYesterday } from '@vegaprotocol/react-helpers';
|
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
import {
|
|
||||||
calcDayVolume,
|
|
||||||
getChange,
|
|
||||||
displayChange,
|
|
||||||
} from '@vegaprotocol/liquidity';
|
|
||||||
|
|
||||||
import type { Candle } from '@vegaprotocol/markets';
|
|
||||||
import { marketCandlesProvider } from '@vegaprotocol/markets';
|
|
||||||
|
|
||||||
const THROTTLE_UPDATE_TIME = 500;
|
|
||||||
|
|
||||||
export const Last24hVolume = ({
|
|
||||||
marketId,
|
|
||||||
decimals,
|
|
||||||
}: {
|
|
||||||
marketId: string;
|
|
||||||
decimals: number;
|
|
||||||
}) => {
|
|
||||||
const [candleVolume, setCandleVolume] = useState<string>();
|
|
||||||
const [volumeChange, setVolumeChange] = useState<string>(' - ');
|
|
||||||
|
|
||||||
const yesterday = useYesterday();
|
|
||||||
|
|
||||||
const yTimestamp = useMemo(() => {
|
|
||||||
return new Date(yesterday).toISOString();
|
|
||||||
}, [yesterday]);
|
|
||||||
|
|
||||||
const variables = useMemo(
|
|
||||||
() => ({
|
|
||||||
marketId: marketId,
|
|
||||||
interval: Schema.Interval.INTERVAL_I1H,
|
|
||||||
since: yTimestamp,
|
|
||||||
}),
|
|
||||||
[marketId, yTimestamp]
|
|
||||||
);
|
|
||||||
|
|
||||||
const variables24hAgo = {
|
|
||||||
marketId: marketId,
|
|
||||||
interval: Schema.Interval.INTERVAL_I1D,
|
|
||||||
since: yTimestamp,
|
|
||||||
};
|
|
||||||
|
|
||||||
const throttledSetCandles = useRef(
|
|
||||||
throttle((data: Candle[]) => {
|
|
||||||
setCandleVolume(calcDayVolume(data));
|
|
||||||
}, THROTTLE_UPDATE_TIME)
|
|
||||||
).current;
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
({ data }: { data: Candle[] | null }) => {
|
|
||||||
if (data) {
|
|
||||||
throttledSetCandles(data);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[throttledSetCandles]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data, error } = useDataProvider({
|
|
||||||
dataProvider: marketCandlesProvider,
|
|
||||||
variables: variables,
|
|
||||||
update,
|
|
||||||
skip: !marketId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const throttledSetVolumeChange = useRef(
|
|
||||||
throttle((candles: Candle[]) => {
|
|
||||||
const candle24hAgo = candles?.[0];
|
|
||||||
setVolumeChange(getChange(data || [], candle24hAgo?.close));
|
|
||||||
}, THROTTLE_UPDATE_TIME)
|
|
||||||
).current;
|
|
||||||
|
|
||||||
const updateCandle24hAgo = useCallback(
|
|
||||||
({ data }: { data: Candle[] | null }) => {
|
|
||||||
if (data) {
|
|
||||||
throttledSetVolumeChange(data);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[throttledSetVolumeChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
useDataProvider({
|
|
||||||
dataProvider: marketCandlesProvider,
|
|
||||||
update: updateCandle24hAgo,
|
|
||||||
variables: variables24hAgo,
|
|
||||||
skip: !marketId || !data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<span className="text-3xl">
|
|
||||||
{!error && candleVolume
|
|
||||||
? addDecimalsFormatNumber(candleVolume, decimals)
|
|
||||||
: '0'}{' '}
|
|
||||||
</span>
|
|
||||||
<span className="text-lg text-greys-light-400">
|
|
||||||
({displayChange(volumeChange)})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './market';
|
|
@ -1,118 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { Icon, HealthBar } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { formatWithAsset } from '@vegaprotocol/liquidity';
|
|
||||||
|
|
||||||
import type * as Schema from '@vegaprotocol/types';
|
|
||||||
import { HealthDialog } from '../../health-dialog';
|
|
||||||
import { Last24hVolume } from '../last-24h-volume';
|
|
||||||
import { Status } from '../../status';
|
|
||||||
import { intentForStatus } from '../../../lib/utils';
|
|
||||||
|
|
||||||
interface Levels {
|
|
||||||
fee: string;
|
|
||||||
commitmentAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface settlementAsset {
|
|
||||||
symbol?: string;
|
|
||||||
decimals?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Market = ({
|
|
||||||
marketId,
|
|
||||||
feeLevels,
|
|
||||||
comittedLiquidity,
|
|
||||||
settlementAsset,
|
|
||||||
targetStake,
|
|
||||||
tradingMode,
|
|
||||||
trigger,
|
|
||||||
}: {
|
|
||||||
marketId: string;
|
|
||||||
feeLevels: Levels[];
|
|
||||||
comittedLiquidity: number;
|
|
||||||
targetStake: string;
|
|
||||||
settlementAsset?: settlementAsset;
|
|
||||||
tradingMode?: Schema.MarketTradingMode;
|
|
||||||
trigger?: Schema.AuctionTrigger;
|
|
||||||
}) => {
|
|
||||||
const [isHealthDialogOpen, setIsHealthDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="border border-greys-light-200 rounded-2xl px-2 py-6">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr
|
|
||||||
className="text-sm text-greys-light-400 text-left font-alpha calt"
|
|
||||||
style={{ fontFeatureSettings: "'liga' off, 'calt' off" }}
|
|
||||||
>
|
|
||||||
<th className="font-medium px-4">{t('Volume (24h)')}</th>
|
|
||||||
<th className="font-medium px-4">{t('Commited Liquidity')}</th>
|
|
||||||
<th className="font-medium px-4">{t('Status')}</th>
|
|
||||||
<th className="font-medium flex items-center px-4">
|
|
||||||
<span>{t('Health')}</span>{' '}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsHealthDialogOpen(true)}
|
|
||||||
aria-label={t('open tooltip')}
|
|
||||||
className="flex ml-1"
|
|
||||||
>
|
|
||||||
<Icon name="info-sign" />
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th className="font-medium">{t('Est. APY')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td className="px-4">
|
|
||||||
<div>
|
|
||||||
{marketId && settlementAsset?.decimals && (
|
|
||||||
<Last24hVolume
|
|
||||||
marketId={marketId}
|
|
||||||
decimals={settlementAsset.decimals}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4">
|
|
||||||
<span className="text-3xl">
|
|
||||||
{comittedLiquidity && settlementAsset
|
|
||||||
? formatWithAsset(`${comittedLiquidity}`, settlementAsset)
|
|
||||||
: '0'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4">
|
|
||||||
<Status
|
|
||||||
trigger={trigger}
|
|
||||||
tradingMode={tradingMode}
|
|
||||||
size="large"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="px-4">
|
|
||||||
{tradingMode && settlementAsset?.decimals && feeLevels && (
|
|
||||||
<HealthBar
|
|
||||||
target={targetStake}
|
|
||||||
decimals={settlementAsset.decimals}
|
|
||||||
levels={feeLevels}
|
|
||||||
intent={intentForStatus(tradingMode)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4">
|
|
||||||
<span className="text-3xl"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<HealthDialog
|
|
||||||
isOpen={isHealthDialogOpen}
|
|
||||||
onChange={() => {
|
|
||||||
setIsHealthDialogOpen(!isHealthDialogOpen);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './providers';
|
|
@ -1,125 +0,0 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { type GetRowIdParams, type ColDef } from 'ag-grid-community';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
|
|
||||||
import {
|
|
||||||
type LiquidityProviderFeeShareFieldsFragment,
|
|
||||||
type LiquidityProvisionFieldsFragment,
|
|
||||||
} from '@vegaprotocol/liquidity';
|
|
||||||
import { formatWithAsset } from '@vegaprotocol/liquidity';
|
|
||||||
|
|
||||||
import { Grid } from '../../grid';
|
|
||||||
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
const formatToHours = ({ value }: { value?: string | null }) => {
|
|
||||||
if (!value) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
const MS_IN_HOUR = 1000 * 60 * 60;
|
|
||||||
const created = new Date(value).getTime();
|
|
||||||
const now = new Date().getTime();
|
|
||||||
return `${Math.round(Math.abs(now - created) / MS_IN_HOUR)}h`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LPProvidersGrid = ({
|
|
||||||
liquidityProviders,
|
|
||||||
settlementAsset,
|
|
||||||
}: {
|
|
||||||
liquidityProviders: LiquidityProvisionFieldsFragment &
|
|
||||||
LiquidityProviderFeeShareFieldsFragment[];
|
|
||||||
settlementAsset: {
|
|
||||||
decimals?: number;
|
|
||||||
symbol?: string;
|
|
||||||
};
|
|
||||||
}) => {
|
|
||||||
const getRowId = useCallback(({ data }: GetRowIdParams) => data.party.id, []);
|
|
||||||
const columnDefs = useMemo<ColDef[]>(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
headerName: t('LPs'),
|
|
||||||
field: 'party.id',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 100,
|
|
||||||
headerTooltip: t('Liquidity providers'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('Duration'),
|
|
||||||
valueFormatter: formatToHours,
|
|
||||||
field: 'createdAt',
|
|
||||||
headerTooltip: t('Time in market'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('Equity-like share'),
|
|
||||||
field: 'equityLikeShare',
|
|
||||||
valueFormatter: ({ value }: { value?: string | null }) => {
|
|
||||||
return value
|
|
||||||
? `${parseFloat(parseFloat(value).toFixed(2)) * 100}%`
|
|
||||||
: '';
|
|
||||||
},
|
|
||||||
headerTooltip: t(
|
|
||||||
'The share of the markets liquidity held - the earlier you commit liquidity the greater % fees you earn'
|
|
||||||
),
|
|
||||||
minWidth: 140,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('committed bond'),
|
|
||||||
field: 'commitmentAmount',
|
|
||||||
valueFormatter: ({ value }: { value?: string | null }) =>
|
|
||||||
value ? formatWithAsset(value, settlementAsset) : '0',
|
|
||||||
headerTooltip: t('The amount of funds allocated to provide liquidity'),
|
|
||||||
minWidth: 140,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('Margin Req.'),
|
|
||||||
field: 'margin',
|
|
||||||
headerTooltip: t(
|
|
||||||
'Margin required for arising positions based on liquidity commitment'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('24h Fees'),
|
|
||||||
field: 'fees',
|
|
||||||
headerTooltip: t(
|
|
||||||
'Total fees earned by the liquidity provider in the last 24 hours'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: t('Fee level'),
|
|
||||||
valueFormatter: ({ value }: { value?: string | null }) => `${value}%`,
|
|
||||||
field: 'fee',
|
|
||||||
headerTooltip: t(
|
|
||||||
"The market's liquidity fee, or the percentage of a trade's value which is collected from the price taker for every trade"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
headerName: t('APY'),
|
|
||||||
field: 'apy',
|
|
||||||
headerTooltip: t(
|
|
||||||
'An annualised estimate based on the total liquidity provision fees and maker fees collected by liquidity providers, the maximum margin needed and maximum commitment (bond) over the course of 7 epochs'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[settlementAsset]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid
|
|
||||||
rowData={liquidityProviders}
|
|
||||||
tooltipShowDelay={500}
|
|
||||||
defaultColDef={{
|
|
||||||
resizable: true,
|
|
||||||
sortable: true,
|
|
||||||
unSortIcon: true,
|
|
||||||
cellClass: ['flex', 'flex-col', 'justify-center'],
|
|
||||||
tooltipComponent: TooltipCellComponent,
|
|
||||||
minWidth: 100,
|
|
||||||
}}
|
|
||||||
columnDefs={columnDefs}
|
|
||||||
getRowId={getRowId}
|
|
||||||
rowHeight={92}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,50 +0,0 @@
|
|||||||
.ag-theme-alpine {
|
|
||||||
--ag-line-height: 24px;
|
|
||||||
--ag-row-hover-color: transparent;
|
|
||||||
--ag-header-background-color: transparent;
|
|
||||||
--ag-odd-row-background-color: transparent;
|
|
||||||
--ag-header-foreground-color: #626262;
|
|
||||||
--ag-secondary-foreground-color: #626262;
|
|
||||||
--ag-font-size: 16px;
|
|
||||||
--ag-background-color: transparent;
|
|
||||||
--ag-range-selection-border-color: transparent;
|
|
||||||
|
|
||||||
font-family: AlphaLyrae, Helvetica Neue, -apple-system, BlinkMacSystemFont,
|
|
||||||
Segoe UI, Roboto, Arial, Noto Sans, sans-serif, Apple Color Emoji,
|
|
||||||
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
|
||||||
font-feature-settings: 'liga' off, 'calt' off;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-cell {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-header {
|
|
||||||
border-bottom: 1px solid #a7a7a7;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-root-wrapper {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-header-row {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-row {
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid #bfccd6;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-root-wrapper-body.ag-layout-normal {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine.row-hover .ag-row:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
import { useRef, useCallback, useEffect } from 'react';
|
|
||||||
import { AgGridReact } from 'ag-grid-react';
|
|
||||||
import {
|
|
||||||
type AgGridReactProps,
|
|
||||||
type AgReactUiProps,
|
|
||||||
type AgGridReact as AgGridReactType,
|
|
||||||
} from 'ag-grid-react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import 'ag-grid-community/styles/ag-grid.css';
|
|
||||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
|
||||||
|
|
||||||
import './grid.scss';
|
|
||||||
|
|
||||||
type Props = (AgGridReactProps | AgReactUiProps) & {
|
|
||||||
isRowClickable?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Grid = ({ isRowClickable, ...props }: Props) => {
|
|
||||||
const gridRef = useRef<AgGridReactType | null>(null);
|
|
||||||
|
|
||||||
const resizeGrid = useCallback(() => {
|
|
||||||
gridRef.current?.api?.sizeColumnsToFit();
|
|
||||||
}, [gridRef]);
|
|
||||||
|
|
||||||
const handleOnGridReady = useCallback(() => {
|
|
||||||
resizeGrid();
|
|
||||||
}, [resizeGrid]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('resize', resizeGrid);
|
|
||||||
return () => window.removeEventListener('resize', resizeGrid);
|
|
||||||
}, [resizeGrid]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgGridReact
|
|
||||||
className={classNames('ag-theme-alpine font-alpha calt h-full', {
|
|
||||||
'row-hover': isRowClickable,
|
|
||||||
})}
|
|
||||||
rowHeight={92}
|
|
||||||
ref={gridRef}
|
|
||||||
onGridReady={handleOnGridReady}
|
|
||||||
suppressRowClickSelection
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './grid';
|
|
@ -1,110 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
import { Dialog, HealthBar } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { intentForStatus } from '../../lib/utils';
|
|
||||||
|
|
||||||
interface HealthDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onChange: (isOpen: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROWS = [
|
|
||||||
{
|
|
||||||
key: '1',
|
|
||||||
title: 'Continuous',
|
|
||||||
copy: 'Markets that have committed liquidity equal or greater than the target stake are trading continuously.',
|
|
||||||
data: {
|
|
||||||
status: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
|
||||||
target: '171320',
|
|
||||||
decimals: 5,
|
|
||||||
levels: [
|
|
||||||
{ fee: '0.6', commitmentAmount: 150000 },
|
|
||||||
{ fee: '1', commitmentAmount: 150000 },
|
|
||||||
{ fee: '2', commitmentAmount: 30000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '2',
|
|
||||||
title: 'Monitoring auction (liquidity)',
|
|
||||||
copy: 'Markets below the target stake will see trading suspended and go into liquidity auction.',
|
|
||||||
data: {
|
|
||||||
status: Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
|
||||||
target: '171320',
|
|
||||||
decimals: 5,
|
|
||||||
levels: [
|
|
||||||
{ fee: '0.6', commitmentAmount: 110000 },
|
|
||||||
{ fee: '1', commitmentAmount: 50000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: '3',
|
|
||||||
title: 'Opening auction',
|
|
||||||
copy: 'A newly created market looking for a target liquidity amount to start trading.',
|
|
||||||
data: {
|
|
||||||
status: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
|
||||||
target: '171320',
|
|
||||||
decimals: 3,
|
|
||||||
levels: [
|
|
||||||
{ fee: '0.6', commitmentAmount: 110000 },
|
|
||||||
{ fee: '1', commitmentAmount: 50000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog size="large" open={isOpen} onChange={onChange}>
|
|
||||||
<h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase">
|
|
||||||
{t('Health')}
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg font-medium font-alpha mb-8">
|
|
||||||
{t(
|
|
||||||
'Market health is a representation of market and liquidity status and how close that market is to moving from one fee level to another.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table className="table-fixed">
|
|
||||||
<thead className="border-b border-greys-light-300">
|
|
||||||
<th className="w-1/2 text-left font-medium font-alpha text-base pb-4 uppercase">
|
|
||||||
{t('Market status')}
|
|
||||||
</th>
|
|
||||||
<th className="w-1/2 text-lef font-medium font-alpha text-base pb-4 uppercase">
|
|
||||||
{t('Liquidity status')}
|
|
||||||
</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{ROWS.map((r, index) => {
|
|
||||||
const isFirstRow = index === 0;
|
|
||||||
return (
|
|
||||||
<tr key={r.key}>
|
|
||||||
<td
|
|
||||||
className={classNames('pr-4 pb-10', { 'pt-8': isFirstRow })}
|
|
||||||
>
|
|
||||||
<h2 className="font-medium font-alpha uppercase text-base">
|
|
||||||
{t(r.title)}
|
|
||||||
</h2>
|
|
||||||
<p className="font-medium font-alpha text-lg">{t(r.copy)}</p>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={classNames('pl-4 pb-10', { 'pt-8': isFirstRow })}
|
|
||||||
>
|
|
||||||
<HealthBar
|
|
||||||
size="large"
|
|
||||||
levels={r.data.levels}
|
|
||||||
target={r.data.target}
|
|
||||||
decimals={r.data.decimals}
|
|
||||||
intent={intentForStatus(r.data.status)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './health-dialog';
|
|
@ -1 +0,0 @@
|
|||||||
export * from './indicator';
|
|
@ -1,24 +0,0 @@
|
|||||||
import type * as Schema from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
import { getColorForStatus } from '../../lib/utils';
|
|
||||||
|
|
||||||
export const Indicator = ({
|
|
||||||
status,
|
|
||||||
opacity,
|
|
||||||
}: {
|
|
||||||
status?: Schema.MarketTradingMode;
|
|
||||||
opacity?: number;
|
|
||||||
}) => {
|
|
||||||
const backgroundColor = status ? getColorForStatus(status) : undefined;
|
|
||||||
return (
|
|
||||||
<div className="inline-block w-2 h-2 mr-1 rounded-full bg-white overflow-hidden shrink-0">
|
|
||||||
<div
|
|
||||||
className="h-full bg-black"
|
|
||||||
style={{
|
|
||||||
opacity,
|
|
||||||
backgroundColor,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,35 +0,0 @@
|
|||||||
.ag-theme-alpine {
|
|
||||||
--ag-line-height: 24px;
|
|
||||||
--ag-row-hover-color: transparent;
|
|
||||||
--ag-header-background-color: #f5f5f5;
|
|
||||||
--ag-odd-row-background-color: transparent;
|
|
||||||
--ag-header-foreground-color: #000;
|
|
||||||
--ag-secondary-foreground-color: #fff;
|
|
||||||
--ag-font-family: 'Helvetica Neue';
|
|
||||||
--ag-font-size: 12px;
|
|
||||||
|
|
||||||
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
||||||
Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-cell {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-header {
|
|
||||||
border: 1px solid #bfccd6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-root-wrapper {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-row {
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid #bfccd6;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ag-theme-alpine .ag-root-wrapper-body.ag-layout-normal {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './navbar';
|
|
@ -1,15 +0,0 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { VegaLogo } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
export const Navbar = () => {
|
|
||||||
return (
|
|
||||||
<div className="px-8 py-4 flex items-stretch border-b border-greys-light-200">
|
|
||||||
<div className="flex gap-4 mr-4 items-center h-full">
|
|
||||||
<Link to="/">
|
|
||||||
<VegaLogo />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 ml-auto"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './status';
|
|
@ -1,96 +0,0 @@
|
|||||||
import { Lozenge, Tooltip } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
|
|
||||||
import { Indicator } from '../indicator';
|
|
||||||
import type { AuctionTrigger } from '@vegaprotocol/types';
|
|
||||||
|
|
||||||
export const Status = ({
|
|
||||||
tradingMode,
|
|
||||||
trigger,
|
|
||||||
size = 'small',
|
|
||||||
}: {
|
|
||||||
tradingMode?: Schema.MarketTradingMode;
|
|
||||||
trigger?: Schema.AuctionTrigger;
|
|
||||||
size?: 'small' | 'large';
|
|
||||||
}) => {
|
|
||||||
const getStatus = () => {
|
|
||||||
if (!tradingMode) return '';
|
|
||||||
if (
|
|
||||||
tradingMode === Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
trigger &&
|
|
||||||
trigger !== Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
|
|
||||||
) {
|
|
||||||
return `${Schema.MarketTradingModeMapping[tradingMode]} - ${Schema.AuctionTriggerMapping[trigger]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Schema.MarketTradingModeMapping[tradingMode];
|
|
||||||
};
|
|
||||||
|
|
||||||
const status = getStatus();
|
|
||||||
const tooltipDescription =
|
|
||||||
tradingMode && getTooltipDescription(tradingMode, trigger);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Tooltip description={tooltipDescription}>
|
|
||||||
<div
|
|
||||||
className={classNames('inline-flex whitespace-normal', {
|
|
||||||
'text-base': size === 'large',
|
|
||||||
'text-sm': size === 'small',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Lozenge className="border border-greys-light-300 bg-greys-light-100 flex items-center">
|
|
||||||
<Indicator status={tradingMode} />
|
|
||||||
{status}
|
|
||||||
</Lozenge>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTooltipDescription = (
|
|
||||||
status: Schema.MarketTradingMode,
|
|
||||||
trigger?: Schema.AuctionTrigger
|
|
||||||
) => {
|
|
||||||
switch (status) {
|
|
||||||
case Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS:
|
|
||||||
return t(
|
|
||||||
'This is the standard trading mode where trades are executed whenever orders are received'
|
|
||||||
);
|
|
||||||
case Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION:
|
|
||||||
return getMonitoringDescriptionTooltip(trigger);
|
|
||||||
case Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION:
|
|
||||||
return t(
|
|
||||||
'This is a new market in an opening auction to determine a fair mid-price before starting continuous trading.'
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMonitoringDescriptionTooltip = (trigger?: AuctionTrigger) => {
|
|
||||||
switch (trigger) {
|
|
||||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET:
|
|
||||||
return t(
|
|
||||||
`This market is in auction until it reaches sufficient liquidity.`
|
|
||||||
);
|
|
||||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS:
|
|
||||||
return t(
|
|
||||||
`This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.`
|
|
||||||
);
|
|
||||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE:
|
|
||||||
return t(`This market is in auction due to high price volatility.`);
|
|
||||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING:
|
|
||||||
return t(
|
|
||||||
`This is a new market in an opening auction to determine a fair mid-price before starting continuous trading`
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,28 +0,0 @@
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
|
||||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
const marketTradingModeStyle = {
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS]: '#00D46E',
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION]: '#CF0064',
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION]: '#0046CD',
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION]: '#CF0064',
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_NO_TRADING]: '#CF0064',
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_SUSPENDED_VIA_GOVERNANCE]: '#CF0064',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getColorForStatus = (status: Schema.MarketTradingMode) =>
|
|
||||||
marketTradingModeStyle[status];
|
|
||||||
|
|
||||||
const marketTradingModeIntent = {
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS]: Intent.Success,
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION]: Intent.Danger,
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION]: Intent.Primary,
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION]: Intent.Danger,
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_NO_TRADING]: Intent.Danger,
|
|
||||||
[Schema.MarketTradingMode.TRADING_MODE_SUSPENDED_VIA_GOVERNANCE]:
|
|
||||||
Intent.Danger,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const intentForStatus = (status: Schema.MarketTradingMode) => {
|
|
||||||
return marketTradingModeIntent[status];
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './router-config';
|
|
@ -1,25 +0,0 @@
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
|
||||||
|
|
||||||
import { Dashboard } from '../components/dashboard';
|
|
||||||
import { Detail } from '../components/detail';
|
|
||||||
|
|
||||||
export const ROUTES = {
|
|
||||||
MARKETS: 'markets',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const routerConfig = [
|
|
||||||
{ path: '/', element: <Dashboard />, icon: '' },
|
|
||||||
{
|
|
||||||
path: ROUTES.MARKETS,
|
|
||||||
name: 'Markets',
|
|
||||||
text: t('Markets'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':marketId',
|
|
||||||
element: <Detail />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
icon: 'trade',
|
|
||||||
isNavItem: true,
|
|
||||||
},
|
|
||||||
];
|
|
@ -1,3 +0,0 @@
|
|||||||
export const environment = {
|
|
||||||
production: true,
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
// This file can be replaced during build by using the `fileReplacements` array.
|
|
||||||
// When building for production, this file is replaced with `environment.prod.ts`.
|
|
||||||
|
|
||||||
export const environment = {
|
|
||||||
production: false,
|
|
||||||
};
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Liquidity Provision Dashboard</title>
|
|
||||||
<base href="/" />
|
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root" class="h-full max-h-full min-h-full"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,15 +0,0 @@
|
|||||||
import { StrictMode } from 'react';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import App from './app/app';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
|
||||||
const root = rootElement && createRoot(rootElement);
|
|
||||||
root?.render(
|
|
||||||
<StrictMode>
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</StrictMode>
|
|
||||||
);
|
|
@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.
|
|
||||||
*
|
|
||||||
* See: https://github.com/zloirock/core-js#babel
|
|
||||||
*/
|
|
||||||
import 'core-js/stable';
|
|
||||||
import 'regenerator-runtime/runtime';
|
|
@ -1,10 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
@apply h-full;
|
|
||||||
|
|
||||||
font-family: 'Helvetica Neue', Helvetica, sans-serif;
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
const { join } = require('path');
|
|
||||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
|
||||||
const theme = require('../../libs/tailwindcss-config/src/theme-lite');
|
|
||||||
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
|
||||||
const vegaCustomClassesLite = require('../../libs/tailwindcss-config/src/vega-custom-classes-lite');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
join(__dirname, 'src/**/*.{js,ts,jsx,tsx}'),
|
|
||||||
'libs/ui-toolkit/src/utils/shared.ts',
|
|
||||||
...createGlobPatternsForDependencies(__dirname),
|
|
||||||
],
|
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
|
||||||
...theme,
|
|
||||||
colors: {
|
|
||||||
...theme.colors,
|
|
||||||
greys: {
|
|
||||||
light: {
|
|
||||||
100: '#F0F0F0',
|
|
||||||
200: '#D2D2D2',
|
|
||||||
300: '#A7A7A7',
|
|
||||||
400: '#626262',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [vegaCustomClasses, vegaCustomClassesLite],
|
|
||||||
};
|
|
@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"types": [
|
|
||||||
"node",
|
|
||||||
"@nx/react/typings/cssmodule.d.ts",
|
|
||||||
"@nx/react/typings/image.d.ts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
|
|
||||||
"../../node_modules/@nx/react/typings/image.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"jest.config.ts",
|
|
||||||
"**/*.spec.ts",
|
|
||||||
"**/*.test.ts",
|
|
||||||
"**/*.spec.tsx",
|
|
||||||
"**/*.test.tsx",
|
|
||||||
"**/*.spec.js",
|
|
||||||
"**/*.test.js",
|
|
||||||
"**/*.spec.jsx",
|
|
||||||
"**/*.test.jsx"
|
|
||||||
],
|
|
||||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"allowJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": true
|
|
||||||
},
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.spec.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"module": "commonjs",
|
|
||||||
"types": [
|
|
||||||
"jest",
|
|
||||||
"node",
|
|
||||||
"@testing-library/jest-dom",
|
|
||||||
"@nx/react/typings/cssmodule.d.ts",
|
|
||||||
"@nx/react/typings/image.d.ts"
|
|
||||||
],
|
|
||||||
"jsx": "react",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"jest.config.ts",
|
|
||||||
"**/*.test.ts",
|
|
||||||
"**/*.spec.ts",
|
|
||||||
"**/*.test.tsx",
|
|
||||||
"**/*.spec.tsx",
|
|
||||||
"**/*.test.js",
|
|
||||||
"**/*.spec.js",
|
|
||||||
"**/*.test.jsx",
|
|
||||||
"**/*.spec.jsx",
|
|
||||||
"**/*.d.ts"
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
|
|
||||||
"../../node_modules/@nx/react/typings/image.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,7 +1,9 @@
|
|||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||||
const theme = require('../../libs/tailwindcss-config/src/theme');
|
const { theme } = require('../../libs/tailwindcss-config/src/theme');
|
||||||
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
const {
|
||||||
|
vegaCustomClasses,
|
||||||
|
} = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 547 B |
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB |
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Vega Protocol - Trading",
|
|
||||||
"short_name": "Console",
|
|
||||||
"description": "Vega Protocol - Trading dApp",
|
|
||||||
"start_url": "/",
|
|
||||||
"display": "standalone",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -16,7 +16,9 @@ import {
|
|||||||
useVegaWallet,
|
useVegaWallet,
|
||||||
useDialogStore,
|
useDialogStore,
|
||||||
} from '@vegaprotocol/wallet-react';
|
} from '@vegaprotocol/wallet-react';
|
||||||
|
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||||
import { Routes } from '../../lib/links';
|
import { Routes } from '../../lib/links';
|
||||||
|
import { Statistics, useStats } from './referral-statistics';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
import { ns, useT } from '../../lib/use-t';
|
import { ns, useT } from '../../lib/use-t';
|
||||||
import { useFundsAvailable } from './hooks/use-funds-available';
|
import { useFundsAvailable } from './hooks/use-funds-available';
|
||||||
@ -24,12 +26,6 @@ import { ViewType, useSidebar } from '../../components/sidebar';
|
|||||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
import { QUSDTooltip } from './qusd-tooltip';
|
import { QUSDTooltip } from './qusd-tooltip';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import { PreviewRefereeStatistics } from './referee-statistics';
|
|
||||||
import {
|
|
||||||
useReferralSet,
|
|
||||||
useIsInReferralSet,
|
|
||||||
} from './hooks/use-find-referral-set';
|
|
||||||
import minBy from 'lodash/minBy';
|
|
||||||
|
|
||||||
const RELOAD_DELAY = 3000;
|
const RELOAD_DELAY = 3000;
|
||||||
|
|
||||||
@ -110,11 +106,9 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
|
|
||||||
const codeField = watch('code');
|
const codeField = watch('code');
|
||||||
|
|
||||||
const {
|
const { data: previewData, loading: previewLoading } = useReferral({
|
||||||
data: previewData,
|
code: validateCode(codeField, t) ? codeField : undefined,
|
||||||
loading: previewLoading,
|
});
|
||||||
isEligible: isPreviewEligible,
|
|
||||||
} = useReferralSet(validateCode(codeField, t) ? codeField : undefined);
|
|
||||||
|
|
||||||
const { send, status } = useSimpleTransaction({
|
const { send, status } = useSimpleTransaction({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -147,14 +141,19 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
* Validates the set a user tries to apply to.
|
* Validates the set a user tries to apply to.
|
||||||
*/
|
*/
|
||||||
const validateSet = useCallback(() => {
|
const validateSet = useCallback(() => {
|
||||||
if (codeField && !previewLoading && previewData && !isPreviewEligible) {
|
if (
|
||||||
|
codeField &&
|
||||||
|
!previewLoading &&
|
||||||
|
previewData &&
|
||||||
|
!previewData.isEligible
|
||||||
|
) {
|
||||||
return t('The code is no longer valid.');
|
return t('The code is no longer valid.');
|
||||||
}
|
}
|
||||||
if (codeField && !previewLoading && !previewData) {
|
if (codeField && !previewLoading && !previewData) {
|
||||||
return t('The code is invalid');
|
return t('The code is invalid');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, [codeField, isPreviewEligible, previewData, previewLoading, t]);
|
}, [codeField, previewData, previewLoading, t]);
|
||||||
|
|
||||||
const noFunds = validateFundsAvailable() !== true ? true : false;
|
const noFunds = validateFundsAvailable() !== true ? true : false;
|
||||||
|
|
||||||
@ -201,6 +200,8 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
// });
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { epochsValue, nextBenefitTierValue } = useStats({ program });
|
||||||
|
|
||||||
// show "code applied" message when successfully applied
|
// show "code applied" message when successfully applied
|
||||||
if (status === 'confirmed') {
|
if (status === 'confirmed') {
|
||||||
return (
|
return (
|
||||||
@ -263,10 +264,9 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// calculate minimum amount of epochs a referee has to be in a set in order
|
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
||||||
// to benefit from it
|
? nextBenefitTierValue.epochs - epochsValue
|
||||||
const firstBenefitTier = minBy(program.benefitTiers, (bt) => bt.epochs);
|
: 0;
|
||||||
const minEpochs = firstBenefitTier ? firstBenefitTier.epochs : 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -335,17 +335,17 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{/* TODO: Re-check plural forms once i18n is updated */}
|
||||||
{previewData && isPreviewEligible ? (
|
{previewData && previewData.isEligible ? (
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h2 className="mb-5 text-2xl">
|
<h2 className="mb-5 text-2xl">
|
||||||
{t(
|
{t(
|
||||||
'youAreJoiningTheGroup',
|
'youAreJoiningTheGroup',
|
||||||
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
|
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
|
||||||
{ count: minEpochs }
|
{ count: nextBenefitTierEpochsValue }
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<PreviewRefereeStatistics setId={codeField} />
|
<Statistics data={previewData} program={program} as="referee" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import { type ApolloError } from '@apollo/client';
|
|
||||||
import { getUserLocale } from '@vegaprotocol/utils';
|
|
||||||
|
|
||||||
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||||
export const GRADIENT =
|
export const GRADIENT =
|
||||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||||
@ -11,19 +8,3 @@ export const REFERRAL_DOCS_LINK =
|
|||||||
export const ABOUT_REFERRAL_DOCS_LINK =
|
export const ABOUT_REFERRAL_DOCS_LINK =
|
||||||
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
||||||
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
|
||||||
|
|
||||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
|
||||||
|
|
||||||
export type StatValue<T> = {
|
|
||||||
value: T;
|
|
||||||
loading: boolean;
|
|
||||||
error?: ApolloError | Error;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const COMPACT_NUMBER_FORMAT = (maximumFractionDigits = 2) =>
|
|
||||||
new Intl.NumberFormat(getUserLocale(), {
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits,
|
|
||||||
notation: 'compact',
|
|
||||||
compactDisplay: 'short',
|
|
||||||
});
|
|
||||||
|
@ -16,16 +16,13 @@ import {
|
|||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||||
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
||||||
|
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { Links, Routes } from '../../lib/links';
|
import { Links, Routes } from '../../lib/links';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import {
|
|
||||||
useFindReferralSet,
|
|
||||||
useIsInReferralSet,
|
|
||||||
} from './hooks/use-find-referral-set';
|
|
||||||
|
|
||||||
export const CreateCodeContainer = () => {
|
export const CreateCodeContainer = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -148,7 +145,7 @@ const CreateCodeDialog = ({
|
|||||||
const t = useT();
|
const t = useT();
|
||||||
const createLink = useLinks(DApp.Governance);
|
const createLink = useLinks(DApp.Governance);
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const { refetch } = useFindReferralSet(pubKey);
|
const { refetch } = useReferral({ pubKey, role: 'referrer' });
|
||||||
const {
|
const {
|
||||||
err,
|
err,
|
||||||
code,
|
code,
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
type ReferralSetsQueryVariables,
|
|
||||||
useReferralSetsQuery,
|
|
||||||
} from './__generated__/ReferralSets';
|
|
||||||
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
|
|
||||||
|
|
||||||
export type Role = 'referrer' | 'referee';
|
|
||||||
type Args = (
|
|
||||||
| { setId: string | undefined }
|
|
||||||
| { pubKey: string | undefined; role: Role }
|
|
||||||
) & {
|
|
||||||
aggregationEpochs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const prepareVariables = (
|
|
||||||
args: Args
|
|
||||||
): [ReferralSetsQueryVariables, boolean] => {
|
|
||||||
const byId = 'setId' in args;
|
|
||||||
const byRole = 'pubKey' in args && 'role' in args;
|
|
||||||
let variables = {};
|
|
||||||
let skip = true;
|
|
||||||
if (byId) {
|
|
||||||
variables = {
|
|
||||||
id: args.setId,
|
|
||||||
};
|
|
||||||
skip = !args.setId;
|
|
||||||
}
|
|
||||||
if (byRole) {
|
|
||||||
if (args.role === 'referee') {
|
|
||||||
variables = { referee: args.pubKey };
|
|
||||||
}
|
|
||||||
if (args.role === 'referrer') {
|
|
||||||
variables = { referrer: args.pubKey };
|
|
||||||
}
|
|
||||||
skip = !args.pubKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [variables, skip];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useFindReferralSet = (pubKey?: string) => {
|
|
||||||
const [referrerVariables, referrerSkip] = prepareVariables({
|
|
||||||
pubKey,
|
|
||||||
role: 'referrer',
|
|
||||||
});
|
|
||||||
const [refereeVariables, refereeSkip] = prepareVariables({
|
|
||||||
pubKey,
|
|
||||||
role: 'referee',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: referrerData,
|
|
||||||
loading: referrerLoading,
|
|
||||||
error: referrerError,
|
|
||||||
refetch: referrerRefetch,
|
|
||||||
} = useReferralSetsQuery({
|
|
||||||
variables: referrerVariables,
|
|
||||||
skip: referrerSkip,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
data: refereeData,
|
|
||||||
loading: refereeLoading,
|
|
||||||
error: refereeError,
|
|
||||||
refetch: refereeRefetch,
|
|
||||||
} = useReferralSetsQuery({
|
|
||||||
variables: refereeVariables,
|
|
||||||
skip: refereeSkip,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
const set =
|
|
||||||
referrerData?.referralSets.edges[0]?.node ||
|
|
||||||
refereeData?.referralSets.edges[0]?.node;
|
|
||||||
const role: Role | undefined = set
|
|
||||||
? set?.referrer === pubKey
|
|
||||||
? 'referrer'
|
|
||||||
: 'referee'
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const { isEligible } = useStakeAvailable(set?.referrer);
|
|
||||||
|
|
||||||
const refetch = useCallback(() => {
|
|
||||||
referrerRefetch();
|
|
||||||
refereeRefetch();
|
|
||||||
}, [refereeRefetch, referrerRefetch]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: set,
|
|
||||||
role,
|
|
||||||
loading: referrerLoading || refereeLoading,
|
|
||||||
error: referrerError || refereeError,
|
|
||||||
refetch,
|
|
||||||
isEligible: set ? isEligible : undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useReferralSet = (setId?: string) => {
|
|
||||||
const [variables, skip] = prepareVariables({ setId });
|
|
||||||
const { data, loading, error, refetch } = useReferralSetsQuery({
|
|
||||||
variables,
|
|
||||||
skip,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
const set = data?.referralSets.edges[0]?.node;
|
|
||||||
const { isEligible } = useStakeAvailable(set?.referrer);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: set,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
isEligible: set ? isEligible : undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useIsInReferralSet = (pubKey: string | undefined) => {
|
|
||||||
const { data } = useFindReferralSet(pubKey);
|
|
||||||
return Boolean(data);
|
|
||||||
};
|
|
@ -1,117 +0,0 @@
|
|||||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
|
||||||
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
|
|
||||||
import { findReferee, useReferees } from './use-referees';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import { type BenefitTier, useReferralProgram } from './use-referral-program';
|
|
||||||
import { type StatValue } from '../constants';
|
|
||||||
import minBy from 'lodash/minBy';
|
|
||||||
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
|
||||||
|
|
||||||
export type RefereeStats = {
|
|
||||||
/** the discount factor -> `discountFactor` ~ `referralDiscountFactor` */
|
|
||||||
discountFactor: StatValue<BigNumber>;
|
|
||||||
/** the benefit tier matching the referee's discount factor */
|
|
||||||
benefitTier: StatValue<BenefitTier | undefined>;
|
|
||||||
/** the next benefit tier after the current referee's tier */
|
|
||||||
nextBenefitTier: StatValue<BenefitTier | undefined>;
|
|
||||||
/** the running volume */
|
|
||||||
runningVolume: StatValue<BigNumber>;
|
|
||||||
/** the number of epochs in set */
|
|
||||||
epochs: StatValue<BigNumber>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ZERO = BigNumber(0);
|
|
||||||
|
|
||||||
export const useRefereeStats = (
|
|
||||||
pubKey: string,
|
|
||||||
setId: string,
|
|
||||||
aggregationEpochs: number
|
|
||||||
): RefereeStats => {
|
|
||||||
const { data, loading, error } = useReferralSetStatsQuery({
|
|
||||||
variables: {
|
|
||||||
code: setId,
|
|
||||||
},
|
|
||||||
skip: !setId || setId.length === 0 || !pubKey || pubKey.length === 0,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
benefitTiers,
|
|
||||||
loading: programLoading,
|
|
||||||
error: programError,
|
|
||||||
} = useReferralProgram();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: epochData,
|
|
||||||
loading: epochsLoading,
|
|
||||||
error: epochsError,
|
|
||||||
} = useEpochInfoQuery({
|
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: refereesData,
|
|
||||||
loading: refereesLoading,
|
|
||||||
error: refereesError,
|
|
||||||
} = useReferees(setId, aggregationEpochs);
|
|
||||||
|
|
||||||
const referee = findReferee(pubKey, refereesData);
|
|
||||||
const stats = removePaginationWrapper(data?.referralSetStats.edges).find(
|
|
||||||
(s) => s.partyId === pubKey
|
|
||||||
);
|
|
||||||
|
|
||||||
const discountFactor = {
|
|
||||||
value: stats?.discountFactor ? BigNumber(stats.discountFactor) : ZERO,
|
|
||||||
loading: loading || refereesLoading,
|
|
||||||
error: error || refereesError,
|
|
||||||
};
|
|
||||||
|
|
||||||
const benefitTier = {
|
|
||||||
value: benefitTiers.find(
|
|
||||||
(t) =>
|
|
||||||
!discountFactor.value.isNaN() &&
|
|
||||||
!isNaN(t.discountFactor) &&
|
|
||||||
t.discountFactor === discountFactor.value.toNumber()
|
|
||||||
),
|
|
||||||
loading: programLoading || discountFactor.loading,
|
|
||||||
error: programError || discountFactor.error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextTier = benefitTier.value?.tier
|
|
||||||
? benefitTier.value.tier + 1
|
|
||||||
: undefined;
|
|
||||||
const nextBenefitTier = {
|
|
||||||
value: nextTier
|
|
||||||
? benefitTiers.find((t) => t.tier === nextTier)
|
|
||||||
: minBy(benefitTiers, (t) => t.tier), // min tier number is lowest tier
|
|
||||||
loading: benefitTier.loading,
|
|
||||||
error: benefitTier.error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const runningVolume = {
|
|
||||||
value: stats?.referralSetRunningNotionalTakerVolume
|
|
||||||
? BigNumber(stats.referralSetRunningNotionalTakerVolume)
|
|
||||||
: ZERO,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const joinedAtEpoch = BigNumber(referee?.atEpoch || '');
|
|
||||||
const currentEpoch = BigNumber(epochData?.epoch.id || '');
|
|
||||||
const epochs = {
|
|
||||||
value:
|
|
||||||
!currentEpoch.isNaN() && !joinedAtEpoch.isNaN()
|
|
||||||
? currentEpoch.minus(joinedAtEpoch)
|
|
||||||
: ZERO,
|
|
||||||
loading: refereesLoading || epochsLoading,
|
|
||||||
error: refereesError || epochsError,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
discountFactor,
|
|
||||||
benefitTier,
|
|
||||||
nextBenefitTier,
|
|
||||||
runningVolume,
|
|
||||||
epochs,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,107 +0,0 @@
|
|||||||
import { type RefereesQuery } from './__generated__/Referees';
|
|
||||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
|
||||||
import { useRefereesQuery } from './__generated__/Referees';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import pick from 'lodash/pick';
|
|
||||||
|
|
||||||
export type Referee = Omit<
|
|
||||||
NonNullable<RefereesQuery['referralSetReferees']['edges'][0]>['node'],
|
|
||||||
'__typename'
|
|
||||||
>;
|
|
||||||
|
|
||||||
/** The properties that can be overwritten by `propertiesOptions`. */
|
|
||||||
type RefereeProperty = keyof Pick<
|
|
||||||
Referee,
|
|
||||||
'totalRefereeGeneratedRewards' | 'totalRefereeNotionalTakerVolume'
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options determining which properties should be overwritten based
|
|
||||||
* on the different `aggregationEpochs`.
|
|
||||||
*/
|
|
||||||
export type PropertiesWithDifferentAggregationEpochs = {
|
|
||||||
properties: RefereeProperty[];
|
|
||||||
aggregationEpochs: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Find referee by its public key (id) */
|
|
||||||
export const findReferee = (pubKey: string, referees: Referee[]) =>
|
|
||||||
referees.find((r) => r.refereeId === pubKey);
|
|
||||||
|
|
||||||
export const useReferees = (
|
|
||||||
id: string | undefined | null,
|
|
||||||
aggregationEpochs: number,
|
|
||||||
propertiesOptions?: PropertiesWithDifferentAggregationEpochs
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
data: refereesData,
|
|
||||||
loading: refereesLoading,
|
|
||||||
error: refereesError,
|
|
||||||
refetch: refereesRefetch,
|
|
||||||
} = useRefereesQuery({
|
|
||||||
variables: {
|
|
||||||
code: id as string,
|
|
||||||
aggregationEpochs,
|
|
||||||
},
|
|
||||||
skip: !id,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
context: { isEnlargedTimeout: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: extraData,
|
|
||||||
loading: extraLoading,
|
|
||||||
error: extraError,
|
|
||||||
refetch: extraRefetch,
|
|
||||||
} = useRefereesQuery({
|
|
||||||
variables: {
|
|
||||||
code: id as string,
|
|
||||||
aggregationEpochs: propertiesOptions?.aggregationEpochs,
|
|
||||||
},
|
|
||||||
skip:
|
|
||||||
// skip if the aggregation epochs are the same
|
|
||||||
!id ||
|
|
||||||
!propertiesOptions?.aggregationEpochs ||
|
|
||||||
propertiesOptions.aggregationEpochs === aggregationEpochs,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
context: { isEnlargedTimeout: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
let referees = [];
|
|
||||||
|
|
||||||
const refereesList = removePaginationWrapper(
|
|
||||||
refereesData?.referralSetReferees.edges
|
|
||||||
);
|
|
||||||
const extraRefereesList = removePaginationWrapper(
|
|
||||||
extraData?.referralSetReferees.edges
|
|
||||||
);
|
|
||||||
|
|
||||||
referees = refereesList.map((r) =>
|
|
||||||
overwriteProperties(r, extraRefereesList, propertiesOptions?.properties)
|
|
||||||
);
|
|
||||||
|
|
||||||
const loading = refereesLoading || extraLoading;
|
|
||||||
const error = refereesError || extraError;
|
|
||||||
const refetch = useCallback(() => {
|
|
||||||
refereesRefetch();
|
|
||||||
extraRefetch();
|
|
||||||
}, [refereesRefetch, extraRefetch]);
|
|
||||||
|
|
||||||
return { data: referees, loading, error, refetch };
|
|
||||||
};
|
|
||||||
|
|
||||||
const overwriteProperties = (
|
|
||||||
referee: Referee,
|
|
||||||
referees: Referee[],
|
|
||||||
properties?: PropertiesWithDifferentAggregationEpochs['properties']
|
|
||||||
) => {
|
|
||||||
let updatedProperties = {};
|
|
||||||
const extraRefereeData = findReferee(referee.refereeId, referees);
|
|
||||||
if (properties && extraRefereeData) {
|
|
||||||
updatedProperties = pick(extraRefereeData, properties);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...referee,
|
|
||||||
...updatedProperties,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,12 +1,8 @@
|
|||||||
import { formatNumber } from '@vegaprotocol/utils';
|
import { formatNumber } from '@vegaprotocol/utils';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import {
|
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
||||||
type ReferralProgramQuery,
|
|
||||||
useReferralProgramQuery,
|
|
||||||
} from './__generated__/CurrentReferralProgram';
|
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import { type ApolloError } from '@apollo/client';
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const MOCK = {
|
const MOCK = {
|
||||||
@ -86,37 +82,7 @@ const MOCK = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProgramDetail = Omit<
|
export const useReferralProgram = () => {
|
||||||
NonNullable<ReferralProgramQuery['currentReferralProgram']>,
|
|
||||||
'benefitTiers' | 'stakingTiers'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type BenefitTier = {
|
|
||||||
tier: number;
|
|
||||||
rewardFactor: number;
|
|
||||||
commission: string;
|
|
||||||
discountFactor: number;
|
|
||||||
discount: string;
|
|
||||||
minimumVolume: number;
|
|
||||||
volume: string;
|
|
||||||
epochs: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StakingTier = {
|
|
||||||
tier: number;
|
|
||||||
minimumStakedTokens: string;
|
|
||||||
referralRewardMultiplier: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReferralProgramData = {
|
|
||||||
benefitTiers: BenefitTier[];
|
|
||||||
stakingTiers: StakingTier[];
|
|
||||||
details: ProgramDetail | undefined;
|
|
||||||
loading: boolean;
|
|
||||||
error?: ApolloError;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useReferralProgram = (): ReferralProgramData => {
|
|
||||||
const { data, loading, error } = useReferralProgramQuery({
|
const { data, loading, error } = useReferralProgramQuery({
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
});
|
});
|
||||||
|
@ -5,22 +5,20 @@ import {
|
|||||||
ToastHeading,
|
ToastHeading,
|
||||||
Button,
|
Button,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useReferral } from './use-referral';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useT } from '../../../lib/use-t';
|
import { useT } from '../../../lib/use-t';
|
||||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Routes } from '../../../lib/links';
|
import { Routes } from '../../../lib/links';
|
||||||
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
||||||
import { useFindReferralSet } from './use-find-referral-set';
|
|
||||||
|
|
||||||
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
|
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
|
||||||
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
|
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
|
||||||
|
|
||||||
const useNonEligibleReferralSet = () => {
|
const useNonEligibleReferralSet = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const { data, loading, role, isEligible, refetch } =
|
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
|
||||||
useFindReferralSet(pubKey);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: epochData,
|
data: epochData,
|
||||||
loading: epochLoading,
|
loading: epochLoading,
|
||||||
@ -38,13 +36,7 @@ const useNonEligibleReferralSet = () => {
|
|||||||
};
|
};
|
||||||
}, [epochRefetch, refetch]);
|
}, [epochRefetch, refetch]);
|
||||||
|
|
||||||
return {
|
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
|
||||||
data,
|
|
||||||
isEligible,
|
|
||||||
role,
|
|
||||||
epoch: epochData?.epoch.id,
|
|
||||||
loading: loading || epochLoading,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useReferralToasts = () => {
|
export const useReferralToasts = () => {
|
||||||
@ -57,16 +49,14 @@ export const useReferralToasts = () => {
|
|||||||
store.update,
|
store.update,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { data, role, isEligible, epoch, loading } =
|
const { data, epoch, loading } = useNonEligibleReferralSet();
|
||||||
useNonEligibleReferralSet();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
data &&
|
data &&
|
||||||
role === 'referee' &&
|
|
||||||
epoch &&
|
epoch &&
|
||||||
!loading &&
|
!loading &&
|
||||||
!isEligible &&
|
!data.isEligible &&
|
||||||
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
|
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
|
||||||
) {
|
) {
|
||||||
const nonEligibleReferralToast: Toast = {
|
const nonEligibleReferralToast: Toast = {
|
||||||
@ -108,11 +98,9 @@ export const useReferralToasts = () => {
|
|||||||
data,
|
data,
|
||||||
epoch,
|
epoch,
|
||||||
hasToast,
|
hasToast,
|
||||||
isEligible,
|
|
||||||
loading,
|
loading,
|
||||||
navigate,
|
navigate,
|
||||||
pathname,
|
pathname,
|
||||||
role,
|
|
||||||
setToast,
|
setToast,
|
||||||
t,
|
t,
|
||||||
updateToast,
|
updateToast,
|
||||||
|
217
apps/trading/client-pages/referrals/hooks/use-referral.ts
Normal file
217
apps/trading/client-pages/referrals/hooks/use-referral.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useRefereesQuery } from './__generated__/Referees';
|
||||||
|
import compact from 'lodash/compact';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
import type {
|
||||||
|
ReferralSetsQuery,
|
||||||
|
ReferralSetsQueryVariables,
|
||||||
|
} from './__generated__/ReferralSets';
|
||||||
|
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||||
|
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
|
||||||
|
|
||||||
|
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||||
|
|
||||||
|
export type Role = 'referrer' | 'referee';
|
||||||
|
type UseReferralArgs = (
|
||||||
|
| { code: string | undefined }
|
||||||
|
| { pubKey: string | undefined; role: Role }
|
||||||
|
) & {
|
||||||
|
aggregationEpochs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareVariables = (
|
||||||
|
args: UseReferralArgs
|
||||||
|
): [ReferralSetsQueryVariables, boolean] => {
|
||||||
|
const byCode = 'code' in args;
|
||||||
|
const byRole = 'pubKey' in args && 'role' in args;
|
||||||
|
let variables = {};
|
||||||
|
let skip = true;
|
||||||
|
if (byCode) {
|
||||||
|
variables = {
|
||||||
|
id: args.code,
|
||||||
|
};
|
||||||
|
skip = !args.code;
|
||||||
|
}
|
||||||
|
if (byRole) {
|
||||||
|
if (args.role === 'referee') {
|
||||||
|
variables = { referee: args.pubKey };
|
||||||
|
}
|
||||||
|
if (args.role === 'referrer') {
|
||||||
|
variables = { referrer: args.pubKey };
|
||||||
|
}
|
||||||
|
skip = !args.pubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [variables, skip];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useReferral = (args: UseReferralArgs) => {
|
||||||
|
const [variables, skip] = prepareVariables(args);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: referralData,
|
||||||
|
loading: referralLoading,
|
||||||
|
error: referralError,
|
||||||
|
refetch: referralRefetch,
|
||||||
|
} = useReferralSetsQuery({
|
||||||
|
variables,
|
||||||
|
skip,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
// A user can only have 1 active referral program at a time
|
||||||
|
const referralSet =
|
||||||
|
referralData?.referralSets.edges &&
|
||||||
|
referralData.referralSets.edges.length > 0
|
||||||
|
? referralData.referralSets.edges[0]?.node
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { isEligible } = useStakeAvailable(referralSet?.referrer);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: refereesData,
|
||||||
|
loading: refereesLoading,
|
||||||
|
error: refereesError,
|
||||||
|
refetch: refereesRefetch,
|
||||||
|
} = useRefereesQuery({
|
||||||
|
variables: {
|
||||||
|
code: referralSet?.id as string,
|
||||||
|
aggregationEpochs:
|
||||||
|
args.aggregationEpochs !== null
|
||||||
|
? args.aggregationEpochs
|
||||||
|
: DEFAULT_AGGREGATION_DAYS,
|
||||||
|
},
|
||||||
|
skip: !referralSet?.id,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
context: { isEnlargedTimeout: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const referees = compact(
|
||||||
|
removePaginationWrapper(refereesData?.referralSetReferees.edges)
|
||||||
|
);
|
||||||
|
|
||||||
|
const refetch = useCallback(() => {
|
||||||
|
referralRefetch();
|
||||||
|
refereesRefetch();
|
||||||
|
}, [refereesRefetch, referralRefetch]);
|
||||||
|
|
||||||
|
const byReferee =
|
||||||
|
'role' in args && 'pubKey' in args && args.role === 'referee';
|
||||||
|
const referee = byReferee
|
||||||
|
? referees.find((r) => r.refereeId === args.pubKey) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const data =
|
||||||
|
referralSet && refereesData
|
||||||
|
? {
|
||||||
|
code: referralSet.id,
|
||||||
|
role: 'role' in args ? args.role : null,
|
||||||
|
referee: referee,
|
||||||
|
referrerId: referralSet.referrer,
|
||||||
|
createdAt: referralSet.createdAt,
|
||||||
|
isEligible,
|
||||||
|
referees,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
loading: referralLoading || refereesLoading,
|
||||||
|
error: referralError || refereesError,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Referee = NonNullable<
|
||||||
|
NonNullable<ReturnType<typeof useReferral>['data']>['referee']
|
||||||
|
>;
|
||||||
|
|
||||||
|
type RefereeProperties = (keyof Referee)[];
|
||||||
|
|
||||||
|
const findReferee = (referee: Referee, referees: Referee[]) =>
|
||||||
|
referees.find((r) => r.refereeId === referee?.refereeId) || referee;
|
||||||
|
|
||||||
|
const updateReferee = (
|
||||||
|
referee: Referee,
|
||||||
|
referees: Referee[],
|
||||||
|
properties: RefereeProperties
|
||||||
|
) => ({
|
||||||
|
...referee,
|
||||||
|
...pick(findReferee(referee, referees), properties),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useUpdateReferees = (
|
||||||
|
referral: ReturnType<typeof useReferral>,
|
||||||
|
aggregationEpochs: number,
|
||||||
|
properties: RefereeProperties,
|
||||||
|
skip?: boolean
|
||||||
|
): ReturnType<typeof useReferral> => {
|
||||||
|
const { data, loading, error, refetch } = useRefereesQuery({
|
||||||
|
variables: {
|
||||||
|
code: referral?.data?.code as string,
|
||||||
|
aggregationEpochs,
|
||||||
|
},
|
||||||
|
skip: skip || !referral?.data?.code,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
context: { isEnlargedTimeout: true },
|
||||||
|
});
|
||||||
|
const refetchAll = useCallback(() => {
|
||||||
|
refetch();
|
||||||
|
referral.refetch();
|
||||||
|
}, [refetch, referral]);
|
||||||
|
if (!referral.data || skip) {
|
||||||
|
return referral;
|
||||||
|
}
|
||||||
|
const referees = compact(
|
||||||
|
removePaginationWrapper(data?.referralSetReferees.edges)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data && {
|
||||||
|
...referral.data,
|
||||||
|
referees: referral.data.referees.map((referee) =>
|
||||||
|
updateReferee(referee, referees, properties)
|
||||||
|
),
|
||||||
|
referee:
|
||||||
|
referral.data.referee &&
|
||||||
|
updateReferee(referral.data.referee, referees, properties),
|
||||||
|
},
|
||||||
|
loading: loading || referral.loading,
|
||||||
|
error: error || referral.error,
|
||||||
|
refetch: refetchAll,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
|
||||||
|
data?.referralSets.edges && data.referralSets.edges.length > 0
|
||||||
|
? data.referralSets.edges[0]?.node
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
export const useIsInReferralSet = (pubKey: string | undefined) => {
|
||||||
|
const [asRefereeVariables, asRefereeSkip] = prepareVariables({
|
||||||
|
pubKey,
|
||||||
|
role: 'referee',
|
||||||
|
});
|
||||||
|
const [asReferrerVariables, asReferrerSkip] = prepareVariables({
|
||||||
|
pubKey,
|
||||||
|
role: 'referrer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: asRefereeData } = useReferralSetsQuery({
|
||||||
|
variables: asRefereeVariables,
|
||||||
|
skip: asRefereeSkip,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: asReferrerData } = useReferralSetsQuery({
|
||||||
|
variables: asReferrerVariables,
|
||||||
|
skip: asReferrerSkip,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
retrieveReferralSetData(asRefereeData) ||
|
||||||
|
retrieveReferralSetData(asReferrerData)
|
||||||
|
);
|
||||||
|
};
|
@ -1,104 +0,0 @@
|
|||||||
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import { useReferees } from './use-referees';
|
|
||||||
import { type StatValue } from '../constants';
|
|
||||||
|
|
||||||
export type ReferrerStats = {
|
|
||||||
/** the base commission -> `rewardFactor` ~ `referralRewardFactor` */
|
|
||||||
baseCommission: StatValue<BigNumber>;
|
|
||||||
/** the staking multiplier -> `rewardsMultiplier` ~ `referralRewardMultiplier` */
|
|
||||||
multiplier: StatValue<BigNumber>;
|
|
||||||
/** the final commission -> base * multiplier */
|
|
||||||
finalCommission: StatValue<BigNumber>;
|
|
||||||
/** the referrer taker volume -> `referrerTakerVolume` */
|
|
||||||
volume: StatValue<BigNumber>;
|
|
||||||
/** the number of referees -> referees query required */
|
|
||||||
referees: StatValue<BigNumber>;
|
|
||||||
/** the total commission -> sum of `totalRefereeGeneratedRewards` */
|
|
||||||
totalCommission: StatValue<BigNumber>;
|
|
||||||
runningVolume: StatValue<BigNumber>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ZERO = BigNumber(0);
|
|
||||||
const ONE = BigNumber(1);
|
|
||||||
|
|
||||||
export const useReferrerStats = (
|
|
||||||
setId: string,
|
|
||||||
aggregationEpochs: number
|
|
||||||
): ReferrerStats => {
|
|
||||||
const { data, loading, error } = useReferralSetStatsQuery({
|
|
||||||
variables: {
|
|
||||||
code: setId,
|
|
||||||
},
|
|
||||||
skip: !setId || setId.length === 0,
|
|
||||||
fetchPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: refereesData,
|
|
||||||
loading: refereesLoading,
|
|
||||||
error: refereesError,
|
|
||||||
} = useReferees(setId, aggregationEpochs);
|
|
||||||
|
|
||||||
const statsAvailable = data?.referralSetStats.edges[0]?.node;
|
|
||||||
|
|
||||||
const baseCommission = {
|
|
||||||
value: statsAvailable ? BigNumber(statsAvailable.rewardFactor) : ZERO,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const multiplier = {
|
|
||||||
value: statsAvailable ? BigNumber(statsAvailable.rewardsMultiplier) : ONE,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const finalCommission = {
|
|
||||||
value: !multiplier.value.isNaN()
|
|
||||||
? baseCommission.value
|
|
||||||
: new BigNumber(multiplier.value).times(baseCommission.value),
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const volume = {
|
|
||||||
value: statsAvailable
|
|
||||||
? BigNumber(statsAvailable.referrerTakerVolume)
|
|
||||||
: ZERO,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const referees = {
|
|
||||||
value: BigNumber(refereesData.length),
|
|
||||||
loading: refereesLoading,
|
|
||||||
error: refereesError,
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalCommission = {
|
|
||||||
value: refereesData
|
|
||||||
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
|
||||||
.reduce((all, r) => all.plus(r), ZERO),
|
|
||||||
loading: refereesLoading,
|
|
||||||
error: refereesError,
|
|
||||||
};
|
|
||||||
|
|
||||||
const runningVolume = {
|
|
||||||
value: statsAvailable?.referralSetRunningNotionalTakerVolume
|
|
||||||
? BigNumber(statsAvailable.referralSetRunningNotionalTakerVolume)
|
|
||||||
: ZERO,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseCommission,
|
|
||||||
multiplier,
|
|
||||||
finalCommission,
|
|
||||||
volume,
|
|
||||||
referees,
|
|
||||||
totalCommission,
|
|
||||||
runningVolume,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,202 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import { useRefereeStats } from './hooks/use-referee-stats';
|
|
||||||
import {
|
|
||||||
BenefitTierTile,
|
|
||||||
DiscountTile,
|
|
||||||
EpochsTile,
|
|
||||||
NextTierEpochsTile,
|
|
||||||
NextTierVolumeTile,
|
|
||||||
RunningVolumeTile,
|
|
||||||
TeamTile,
|
|
||||||
} from './tiles';
|
|
||||||
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
|
||||||
import { CodeTile } from './tile';
|
|
||||||
import { useT } from '../../lib/use-t';
|
|
||||||
import { ApplyCodeForm } from './apply-code-form';
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
|
||||||
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
|
||||||
import { useReferralSet } from './hooks/use-find-referral-set';
|
|
||||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import minBy from 'lodash/minBy';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
|
|
||||||
export const RefereeStatistics = ({
|
|
||||||
aggregationEpochs,
|
|
||||||
setId,
|
|
||||||
pubKey,
|
|
||||||
referrerPubKey,
|
|
||||||
}: {
|
|
||||||
/** The aggregation epochs used to calculate statistics. */
|
|
||||||
aggregationEpochs: number;
|
|
||||||
/** The set id (code). */
|
|
||||||
setId: string;
|
|
||||||
/** The referee public key. */
|
|
||||||
pubKey: string;
|
|
||||||
/** The referrer's public key. */
|
|
||||||
referrerPubKey: string;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const {
|
|
||||||
benefitTier,
|
|
||||||
discountFactor,
|
|
||||||
epochs,
|
|
||||||
nextBenefitTier,
|
|
||||||
runningVolume,
|
|
||||||
} = useRefereeStats(pubKey, setId, aggregationEpochs);
|
|
||||||
|
|
||||||
const { isEligible } = useStakeAvailable(referrerPubKey);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
data-testid="referral-statistics"
|
|
||||||
data-as="referee"
|
|
||||||
className="relative mx-auto mb-20"
|
|
||||||
>
|
|
||||||
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
|
||||||
{/** TEAM TILE - referral set id is the same as team id */}
|
|
||||||
<TeamTile teamId={setId} />
|
|
||||||
{/** TILES ROW 1 */}
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
||||||
<BenefitTierTile
|
|
||||||
benefitTier={benefitTier}
|
|
||||||
nextBenefitTier={nextBenefitTier}
|
|
||||||
/>
|
|
||||||
<RunningVolumeTile
|
|
||||||
aggregationEpochs={aggregationEpochs}
|
|
||||||
runningVolume={runningVolume}
|
|
||||||
/>
|
|
||||||
<CodeTile code={setId} />
|
|
||||||
</div>
|
|
||||||
{/** TILES ROW 2 */}
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<DiscountTile discountFactor={discountFactor} />
|
|
||||||
<NextTierVolumeTile
|
|
||||||
nextBenefitTier={nextBenefitTier}
|
|
||||||
runningVolume={runningVolume}
|
|
||||||
/>
|
|
||||||
<EpochsTile epochs={epochs} />
|
|
||||||
<NextTierEpochsTile
|
|
||||||
epochs={epochs}
|
|
||||||
nextBenefitTier={nextBenefitTier}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/** ELIGIBILITY WARNING */}
|
|
||||||
{!isEligible ? (
|
|
||||||
<div
|
|
||||||
data-testid="referral-eligibility-warning"
|
|
||||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
|
||||||
>
|
|
||||||
<h2 className="text-2xl mb-2">
|
|
||||||
{t('Referral code no longer valid')}
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{!isEligible && <ApplyCodeForm />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PreviewRefereeStatistics = ({ setId }: { setId: string }) => {
|
|
||||||
const program = useReferralProgram();
|
|
||||||
const aggregationEpochs =
|
|
||||||
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
|
|
||||||
|
|
||||||
const { pubKey } = useVegaWallet();
|
|
||||||
|
|
||||||
const { data: referralSet, loading } = useReferralSet(setId);
|
|
||||||
|
|
||||||
const { epochs, runningVolume } = useRefereeStats(
|
|
||||||
pubKey || '',
|
|
||||||
referralSet?.id || '',
|
|
||||||
aggregationEpochs
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-testid="referral-statistics"
|
|
||||||
data-as="referee"
|
|
||||||
data-preview
|
|
||||||
className="relative mx-auto mb-20"
|
|
||||||
>
|
|
||||||
<Loader size="small" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!referralSet) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stat = <T,>(value: T) => ({
|
|
||||||
value,
|
|
||||||
loading: false,
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstBenefitTier = stat(minBy(program.benefitTiers, (bt) => bt.epochs));
|
|
||||||
|
|
||||||
const nextBenefitTier = stat(
|
|
||||||
program.benefitTiers.find(
|
|
||||||
(bt) =>
|
|
||||||
bt.tier ===
|
|
||||||
(firstBenefitTier.value?.tier
|
|
||||||
? firstBenefitTier.value.tier + 1
|
|
||||||
: undefined)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const discountFactor = stat(
|
|
||||||
firstBenefitTier.value?.discountFactor
|
|
||||||
? BigNumber(firstBenefitTier.value?.discountFactor)
|
|
||||||
: BigNumber(0)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-testid="referral-statistics"
|
|
||||||
data-as="referee"
|
|
||||||
data-preview
|
|
||||||
className="relative mx-auto mb-20"
|
|
||||||
>
|
|
||||||
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
|
||||||
{/** TEAM TILE - referral set id is the same as team id */}
|
|
||||||
<TeamTile teamId={setId} />
|
|
||||||
{/** TILES ROW 1 */}
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
||||||
<BenefitTierTile
|
|
||||||
benefitTier={firstBenefitTier}
|
|
||||||
nextBenefitTier={nextBenefitTier}
|
|
||||||
/>
|
|
||||||
<RunningVolumeTile
|
|
||||||
aggregationEpochs={aggregationEpochs}
|
|
||||||
runningVolume={runningVolume}
|
|
||||||
/>
|
|
||||||
<CodeTile code={setId} />
|
|
||||||
</div>
|
|
||||||
{/** TILES ROW 2 */}
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<DiscountTile discountFactor={discountFactor} />
|
|
||||||
<NextTierVolumeTile
|
|
||||||
nextBenefitTier={nextBenefitTier}
|
|
||||||
runningVolume={runningVolume}
|
|
||||||
/>
|
|
||||||
<EpochsTile epochs={epochs} />
|
|
||||||
<NextTierEpochsTile
|
|
||||||
epochs={epochs}
|
|
||||||
nextBenefitTier={nextBenefitTier}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,146 +0,0 @@
|
|||||||
import { useLayoutEffect, useRef, useState } from 'react';
|
|
||||||
import { ns, useT } from '../../lib/use-t';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
|
||||||
Loader,
|
|
||||||
Tooltip,
|
|
||||||
VegaIcon,
|
|
||||||
VegaIconNames,
|
|
||||||
truncateMiddle,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { Table } from '../../components/table';
|
|
||||||
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
|
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import { QUSDTooltip } from './qusd-tooltip';
|
|
||||||
import { type Referee, useReferees } from './hooks/use-referees';
|
|
||||||
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
|
||||||
|
|
||||||
export const Referees = ({
|
|
||||||
setId,
|
|
||||||
aggregationEpochs,
|
|
||||||
}: {
|
|
||||||
setId: string;
|
|
||||||
aggregationEpochs: number;
|
|
||||||
}) => {
|
|
||||||
const { data, loading } = useReferees(setId, aggregationEpochs, {
|
|
||||||
// get total referree generated rewards for the last 30 days
|
|
||||||
aggregationEpochs: DEFAULT_AGGREGATION_DAYS,
|
|
||||||
properties: ['totalRefereeGeneratedRewards'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loader size="small" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <RefereesTable data={data} aggregationEpochs={aggregationEpochs} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RefereesTable = ({
|
|
||||||
data: referees,
|
|
||||||
aggregationEpochs,
|
|
||||||
}: {
|
|
||||||
data: Referee[];
|
|
||||||
aggregationEpochs: number;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const tableRef = useRef<HTMLTableElement>(null);
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
|
||||||
setCollapsed(true);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Referees (only for referrer view) */}
|
|
||||||
{referees.length > 0 && (
|
|
||||||
<div className="mt-20 mb-20">
|
|
||||||
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
collapsed && [
|
|
||||||
'relative max-h-96 overflow-hidden',
|
|
||||||
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
|
|
||||||
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className={classNames(
|
|
||||||
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
|
|
||||||
{
|
|
||||||
hidden: !collapsed,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onClick={() => setCollapsed(false)}
|
|
||||||
>
|
|
||||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
|
|
||||||
</button>
|
|
||||||
<Table
|
|
||||||
ref={tableRef}
|
|
||||||
columns={[
|
|
||||||
{ name: 'party', displayName: t('Trader') },
|
|
||||||
{ name: 'joined', displayName: t('Date Joined') },
|
|
||||||
{
|
|
||||||
name: 'volume',
|
|
||||||
displayName: t(
|
|
||||||
'volumeLastEpochs',
|
|
||||||
'Volume (last {{count}} epochs)',
|
|
||||||
{
|
|
||||||
count: aggregationEpochs,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// NOTE: This should be gotten for the last 30 days regardless of the program's window length
|
|
||||||
name: 'commission',
|
|
||||||
displayName: (
|
|
||||||
<Trans
|
|
||||||
i18nKey="referralStatisticsCommission"
|
|
||||||
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
|
||||||
components={[
|
|
||||||
<QUSDTooltip key="0" />,
|
|
||||||
<Tooltip
|
|
||||||
key="1"
|
|
||||||
description={t(
|
|
||||||
'Depending on data node retention you may not be able see the full 30 days'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>last 30 epochs</span>
|
|
||||||
</Tooltip>,
|
|
||||||
]}
|
|
||||||
values={{
|
|
||||||
count: DEFAULT_AGGREGATION_DAYS,
|
|
||||||
}}
|
|
||||||
ns={ns}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
data={sortBy(
|
|
||||||
referees.map((r) => ({
|
|
||||||
party: (
|
|
||||||
<span title={r.refereeId}>
|
|
||||||
{truncateMiddle(r.refereeId)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
|
|
||||||
volume: Number(r.totalRefereeNotionalTakerVolume),
|
|
||||||
commission: Number(r.totalRefereeGeneratedRewards),
|
|
||||||
})),
|
|
||||||
(r) => r.volume
|
|
||||||
)
|
|
||||||
.map((r) => ({
|
|
||||||
...r,
|
|
||||||
volume: formatNumber(r.volume, 0),
|
|
||||||
commission: formatNumber(r.commission, 0),
|
|
||||||
}))
|
|
||||||
.reverse()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,58 +1,597 @@
|
|||||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import minBy from 'lodash/minBy';
|
||||||
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import compact from 'lodash/compact';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
truncateMiddle,
|
||||||
|
TextChildrenTooltip as Tooltip,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
import { ApplyCodeFormContainer } from './apply-code-form';
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatNumber,
|
||||||
|
getDateFormat,
|
||||||
|
getDateTimeFormat,
|
||||||
|
getUserLocale,
|
||||||
|
removePaginationWrapper,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
|
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
||||||
|
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||||
|
import { useT, ns } from '../../lib/use-t';
|
||||||
|
import { useTeam } from '../../lib/hooks/use-team';
|
||||||
|
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||||
|
import { TeamStats } from '../../components/competitions/team-stats';
|
||||||
|
import { Table } from '../../components/table';
|
||||||
|
import {
|
||||||
|
DEFAULT_AGGREGATION_DAYS,
|
||||||
|
useReferral,
|
||||||
|
useUpdateReferees,
|
||||||
|
} from './hooks/use-referral';
|
||||||
|
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
import { useFindReferralSet } from './hooks/use-find-referral-set';
|
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||||
import { Referees } from './referees';
|
import { QUSDTooltip } from './qusd-tooltip';
|
||||||
import { ReferrerStatistics } from './referrer-statistics';
|
import { CodeTile, StatTile, Tile } from './tile';
|
||||||
import { RefereeStatistics } from './referee-statistics';
|
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
|
||||||
import { DEFAULT_AGGREGATION_DAYS } from './constants';
|
|
||||||
|
|
||||||
export const ReferralStatistics = () => {
|
export const ReferralStatistics = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
const program = useReferralProgram();
|
const program = useReferralProgram();
|
||||||
|
|
||||||
const {
|
const { data: referee, refetch: refereeRefetch } = useReferral({
|
||||||
data: referralSet,
|
pubKey,
|
||||||
loading: referralSetLoading,
|
role: 'referee',
|
||||||
role,
|
aggregationEpochs: program.details?.windowLength,
|
||||||
refetch,
|
});
|
||||||
} = useFindReferralSet(pubKey);
|
|
||||||
|
|
||||||
if (referralSetLoading) {
|
const { data: referrer, refetch: referrerRefetch } = useUpdateReferees(
|
||||||
return <Loader size="small" />;
|
useReferral({
|
||||||
}
|
pubKey,
|
||||||
|
role: 'referrer',
|
||||||
|
aggregationEpochs: program.details?.windowLength,
|
||||||
|
}),
|
||||||
|
DEFAULT_AGGREGATION_DAYS,
|
||||||
|
['totalRefereeGeneratedRewards'],
|
||||||
|
DEFAULT_AGGREGATION_DAYS === program.details?.windowLength
|
||||||
|
);
|
||||||
|
|
||||||
const aggregationEpochs =
|
const refetch = useCallback(() => {
|
||||||
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
|
refereeRefetch();
|
||||||
|
referrerRefetch();
|
||||||
|
}, [refereeRefetch, referrerRefetch]);
|
||||||
|
|
||||||
if (referralSet?.id && role === 'referrer') {
|
if (referee?.code) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReferrerStatistics
|
<Statistics data={referee} program={program} as="referee" />
|
||||||
aggregationEpochs={aggregationEpochs}
|
{!referee.isEligible && <ApplyCodeForm />}
|
||||||
createdAt={referralSet.createdAt}
|
|
||||||
setId={referralSet.id}
|
|
||||||
/>
|
|
||||||
<Referees
|
|
||||||
setId={referralSet.id}
|
|
||||||
aggregationEpochs={aggregationEpochs}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pubKey && referralSet?.id && role === 'referee') {
|
if (referrer?.code) {
|
||||||
return (
|
return (
|
||||||
<RefereeStatistics
|
<>
|
||||||
aggregationEpochs={aggregationEpochs}
|
<Statistics data={referrer} program={program} as="referrer" />
|
||||||
pubKey={pubKey}
|
<RefereesTable data={referrer} program={program} />
|
||||||
referrerPubKey={referralSet.referrer}
|
</>
|
||||||
setId={referralSet.id}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ApplyCodeFormContainer onSuccess={refetch} />;
|
return <ApplyCodeFormContainer onSuccess={refetch} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useStats = ({
|
||||||
|
data,
|
||||||
|
program,
|
||||||
|
}: {
|
||||||
|
data?: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||||
|
program: ReturnType<typeof useReferralProgram>;
|
||||||
|
}) => {
|
||||||
|
const { benefitTiers } = program;
|
||||||
|
const { data: epochData } = useEpochInfoQuery({
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
const { data: statsData } = useReferralSetStatsQuery({
|
||||||
|
variables: {
|
||||||
|
code: data?.code || '',
|
||||||
|
},
|
||||||
|
skip: !data?.code,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentEpoch = Number(epochData?.epoch.id);
|
||||||
|
|
||||||
|
const stats =
|
||||||
|
statsData?.referralSetStats.edges &&
|
||||||
|
compact(removePaginationWrapper(statsData.referralSetStats.edges));
|
||||||
|
const refereeInfo = data?.referee;
|
||||||
|
const refereeStats = stats?.find(
|
||||||
|
(r) => r.partyId === data?.referee?.refereeId
|
||||||
|
);
|
||||||
|
|
||||||
|
const statsAvailable = stats && stats.length > 0 && stats[0];
|
||||||
|
const baseCommissionValue = statsAvailable
|
||||||
|
? Number(statsAvailable.rewardFactor)
|
||||||
|
: 0;
|
||||||
|
const runningVolumeValue = statsAvailable
|
||||||
|
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
|
||||||
|
: 0;
|
||||||
|
const referrerVolumeValue = statsAvailable
|
||||||
|
? Number(statsAvailable.referrerTakerVolume)
|
||||||
|
: 0;
|
||||||
|
const multiplier = statsAvailable
|
||||||
|
? Number(statsAvailable.rewardsMultiplier)
|
||||||
|
: 1;
|
||||||
|
const finalCommissionValue = isNaN(multiplier)
|
||||||
|
? baseCommissionValue
|
||||||
|
: new BigNumber(multiplier).times(baseCommissionValue).toNumber();
|
||||||
|
|
||||||
|
const discountFactorValue = refereeStats?.discountFactor
|
||||||
|
? Number(refereeStats.discountFactor)
|
||||||
|
: 0;
|
||||||
|
const currentBenefitTierValue = benefitTiers.find(
|
||||||
|
(t) =>
|
||||||
|
!isNaN(discountFactorValue) &&
|
||||||
|
!isNaN(t.discountFactor) &&
|
||||||
|
t.discountFactor === discountFactorValue
|
||||||
|
);
|
||||||
|
const nextBenefitTierValue = currentBenefitTierValue
|
||||||
|
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
|
||||||
|
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
|
||||||
|
const epochsValue =
|
||||||
|
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||||
|
? currentEpoch - refereeInfo?.atEpoch
|
||||||
|
: 0;
|
||||||
|
const nextBenefitTierVolumeValue = nextBenefitTierValue
|
||||||
|
? nextBenefitTierValue.minimumVolume - runningVolumeValue
|
||||||
|
: 0;
|
||||||
|
const nextBenefitTierEpochsValue = nextBenefitTierValue
|
||||||
|
? nextBenefitTierValue.epochs - epochsValue
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseCommissionValue,
|
||||||
|
runningVolumeValue,
|
||||||
|
referrerVolumeValue,
|
||||||
|
multiplier,
|
||||||
|
finalCommissionValue,
|
||||||
|
discountFactorValue,
|
||||||
|
currentBenefitTierValue,
|
||||||
|
nextBenefitTierValue,
|
||||||
|
epochsValue,
|
||||||
|
nextBenefitTierVolumeValue,
|
||||||
|
nextBenefitTierEpochsValue,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Statistics = ({
|
||||||
|
data,
|
||||||
|
program,
|
||||||
|
as,
|
||||||
|
}: {
|
||||||
|
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||||
|
program: ReturnType<typeof useReferralProgram>;
|
||||||
|
as: 'referrer' | 'referee';
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const {
|
||||||
|
baseCommissionValue,
|
||||||
|
runningVolumeValue,
|
||||||
|
referrerVolumeValue,
|
||||||
|
multiplier,
|
||||||
|
finalCommissionValue,
|
||||||
|
discountFactorValue,
|
||||||
|
currentBenefitTierValue,
|
||||||
|
epochsValue,
|
||||||
|
nextBenefitTierValue,
|
||||||
|
nextBenefitTierVolumeValue,
|
||||||
|
nextBenefitTierEpochsValue,
|
||||||
|
} = useStats({ data, program });
|
||||||
|
|
||||||
|
const isApplyCodePreview = data.referee === null;
|
||||||
|
|
||||||
|
const { benefitTiers } = useReferralProgram();
|
||||||
|
|
||||||
|
const { stakeAvailable, isEligible } = useStakeAvailable();
|
||||||
|
const { details } = program;
|
||||||
|
|
||||||
|
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
notation: 'compact',
|
||||||
|
compactDisplay: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseCommissionTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('Base commission rate')}
|
||||||
|
description={t(
|
||||||
|
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
|
||||||
|
{
|
||||||
|
runningVolume: compactNumFormat.format(runningVolumeValue),
|
||||||
|
epochs: (
|
||||||
|
details?.windowLength || DEFAULT_AGGREGATION_DAYS
|
||||||
|
).toString(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
testId="base-commission-rate"
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{baseCommissionValue * 100}%
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
|
||||||
|
const stakingMultiplierTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('Staking multiplier')}
|
||||||
|
testId="staking-multiplier"
|
||||||
|
description={
|
||||||
|
<span
|
||||||
|
className={classNames({
|
||||||
|
'text-vega-red': !isEligible,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('{{amount}} $VEGA staked', {
|
||||||
|
amount: addDecimalsFormatNumber(
|
||||||
|
stakeAvailable?.toString() || 0,
|
||||||
|
18
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{multiplier || t('None')}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
const baseCommissionFormatted = BigNumber(baseCommissionValue)
|
||||||
|
.times(100)
|
||||||
|
.toString();
|
||||||
|
const finalCommissionFormatted = new BigNumber(finalCommissionValue)
|
||||||
|
.times(100)
|
||||||
|
.toString();
|
||||||
|
const finalCommissionTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('Final commission rate')}
|
||||||
|
description={
|
||||||
|
!isNaN(multiplier)
|
||||||
|
? `(${baseCommissionFormatted}% ⨉ ${multiplier} = ${finalCommissionFormatted}%)`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
testId="final-commission-rate"
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{finalCommissionFormatted}%
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
const numberOfTradersValue = data.referees.length;
|
||||||
|
const numberOfTradersTile = (
|
||||||
|
<StatTile title={t('Number of traders')} testId="number-of-traders">
|
||||||
|
{numberOfTradersValue}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeTile = (
|
||||||
|
<CodeTile
|
||||||
|
code={data?.code}
|
||||||
|
createdAt={getDateFormat().format(new Date(data.createdAt))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const referrerVolumeTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||||
|
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||||
|
})}
|
||||||
|
testId="my-volume"
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{compactNumFormat.format(referrerVolumeValue)}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalCommissionValue = data.referees
|
||||||
|
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
|
||||||
|
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||||
|
const totalCommissionTile = (
|
||||||
|
<StatTile
|
||||||
|
testId="total-commission"
|
||||||
|
title={
|
||||||
|
<Trans
|
||||||
|
i18nKey="totalCommission"
|
||||||
|
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
||||||
|
values={{
|
||||||
|
count: DEFAULT_AGGREGATION_DAYS,
|
||||||
|
}}
|
||||||
|
components={[
|
||||||
|
<Tooltip
|
||||||
|
key="1"
|
||||||
|
description={t(
|
||||||
|
'Depending on data node retention you may not be able see the full 30 days'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
last 30 epochs
|
||||||
|
</Tooltip>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
description={<QUSDTooltip />}
|
||||||
|
>
|
||||||
|
{formatNumber(totalCommissionValue, 0)}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentBenefitTierTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('Current tier')}
|
||||||
|
testId="current-tier"
|
||||||
|
description={
|
||||||
|
nextBenefitTierValue?.tier
|
||||||
|
? t('(Next tier: {{nextTier}})', {
|
||||||
|
nextTier: nextBenefitTierValue?.tier,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{isApplyCodePreview
|
||||||
|
? currentBenefitTierValue?.tier || benefitTiers[0]?.tier || 'None'
|
||||||
|
: currentBenefitTierValue?.tier || 'None'}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
const discountFactorTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('Discount')}
|
||||||
|
testId="discount"
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{isApplyCodePreview && benefitTiers.length >= 1
|
||||||
|
? benefitTiers[0].discountFactor * 100
|
||||||
|
: discountFactorValue * 100}
|
||||||
|
%
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
const runningVolumeTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t(
|
||||||
|
'runningNotionalOverEpochs',
|
||||||
|
'Combined volume (last {{count}} epochs)',
|
||||||
|
{
|
||||||
|
count: details?.windowLength,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
testId="combined-volume"
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{compactNumFormat.format(runningVolumeValue)}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
const epochsTile = (
|
||||||
|
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
|
||||||
|
{epochsValue}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
const nextTierVolumeTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('Volume to next tier')}
|
||||||
|
testId="vol-to-next-tier"
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{nextBenefitTierVolumeValue <= 0
|
||||||
|
? '0'
|
||||||
|
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
const nextTierEpochsTile = (
|
||||||
|
<StatTile
|
||||||
|
title={t('Epochs to next tier')}
|
||||||
|
testId="epochs-to-next-tier"
|
||||||
|
overrideWithNoProgram={!details}
|
||||||
|
>
|
||||||
|
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||||
|
</StatTile>
|
||||||
|
);
|
||||||
|
|
||||||
|
const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
|
||||||
|
<div
|
||||||
|
data-testid="referral-eligibility-warning"
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const referrerTiles = (
|
||||||
|
<>
|
||||||
|
<Team teamId={data.code} />
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
|
{baseCommissionTile}
|
||||||
|
{stakingMultiplierTile}
|
||||||
|
{finalCommissionTile}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{codeTile}
|
||||||
|
{referrerVolumeTile}
|
||||||
|
{numberOfTradersTile}
|
||||||
|
{totalCommissionTile}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const refereeTiles = (
|
||||||
|
<>
|
||||||
|
<Team teamId={data.code} />
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
|
{currentBenefitTierTile}
|
||||||
|
{runningVolumeTile}
|
||||||
|
{codeTile}
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{discountFactorTile}
|
||||||
|
{nextTierVolumeTile}
|
||||||
|
{epochsTile}
|
||||||
|
{nextTierEpochsTile}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="referral-statistics"
|
||||||
|
data-as={as}
|
||||||
|
className="relative mx-auto mb-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
|
||||||
|
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{as === 'referrer' && referrerTiles}
|
||||||
|
{as === 'referee' && refereeTiles}
|
||||||
|
</div>
|
||||||
|
{eligibilityWarningOverlay}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RefereesTable = ({
|
||||||
|
data,
|
||||||
|
program,
|
||||||
|
}: {
|
||||||
|
data: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||||
|
program: ReturnType<typeof useReferralProgram>;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const tableRef = useRef<HTMLTableElement>(null);
|
||||||
|
const { details } = program;
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
|
||||||
|
setCollapsed(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Referees (only for referrer view) */}
|
||||||
|
{data.referees.length > 0 && (
|
||||||
|
<div className="mt-20 mb-20">
|
||||||
|
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
collapsed && [
|
||||||
|
'relative max-h-96 overflow-hidden',
|
||||||
|
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
|
||||||
|
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
|
||||||
|
{
|
||||||
|
hidden: !collapsed,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => setCollapsed(false)}
|
||||||
|
>
|
||||||
|
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
|
||||||
|
</button>
|
||||||
|
<Table
|
||||||
|
ref={tableRef}
|
||||||
|
columns={[
|
||||||
|
{ name: 'party', displayName: t('Trader') },
|
||||||
|
{ name: 'joined', displayName: t('Date Joined') },
|
||||||
|
{
|
||||||
|
name: 'volume',
|
||||||
|
displayName: t(
|
||||||
|
'volumeLastEpochs',
|
||||||
|
'Volume (last {{count}} epochs)',
|
||||||
|
{
|
||||||
|
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'commission',
|
||||||
|
displayName: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="referralStatisticsCommission"
|
||||||
|
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
||||||
|
components={[
|
||||||
|
<QUSDTooltip key="0" />,
|
||||||
|
<Tooltip
|
||||||
|
key="1"
|
||||||
|
description={t(
|
||||||
|
'Depending on data node retention you may not be able see the full 30 days'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
last 30 epochs
|
||||||
|
</Tooltip>,
|
||||||
|
]}
|
||||||
|
values={{
|
||||||
|
count: DEFAULT_AGGREGATION_DAYS,
|
||||||
|
}}
|
||||||
|
ns={ns}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={sortBy(
|
||||||
|
data.referees.map((r) => ({
|
||||||
|
party: (
|
||||||
|
<span title={r.refereeId}>
|
||||||
|
{truncateMiddle(r.refereeId)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
|
||||||
|
volume: Number(r.totalRefereeNotionalTakerVolume),
|
||||||
|
commission: Number(r.totalRefereeGeneratedRewards),
|
||||||
|
})),
|
||||||
|
(r) => r.volume
|
||||||
|
)
|
||||||
|
.map((r) => ({
|
||||||
|
...r,
|
||||||
|
volume: formatNumber(r.volume, 0),
|
||||||
|
commission: formatNumber(r.commission, 0),
|
||||||
|
}))
|
||||||
|
.reverse()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Team = ({ teamId }: { teamId?: string }) => {
|
||||||
|
const { team, members } = useTeam(teamId);
|
||||||
|
const { data: games } = useGames(teamId);
|
||||||
|
|
||||||
|
if (!team) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tile className="flex gap-3 lg:gap-4">
|
||||||
|
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||||
|
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
||||||
|
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
|
||||||
|
<TeamStats
|
||||||
|
members={members}
|
||||||
|
games={areTeamGames(games) ? games : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -10,12 +10,12 @@ import { TabLink } from './buttons';
|
|||||||
import { Outlet, useMatch } from 'react-router-dom';
|
import { Outlet, useMatch } from 'react-router-dom';
|
||||||
import { Routes } from '../../lib/links';
|
import { Routes } from '../../lib/links';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
|
import { useReferral } from './hooks/use-referral';
|
||||||
import { REFERRAL_DOCS_LINK } from './constants';
|
import { REFERRAL_DOCS_LINK } from './constants';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
import { useFindReferralSet } from './hooks/use-find-referral-set';
|
|
||||||
|
|
||||||
const Nav = () => {
|
const Nav = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -34,9 +34,26 @@ export const Referrals = () => {
|
|||||||
const t = useT();
|
const t = useT();
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
const { data, loading, error } = useFindReferralSet(pubKey);
|
const {
|
||||||
|
data: referee,
|
||||||
|
loading: refereeLoading,
|
||||||
|
error: refereeError,
|
||||||
|
} = useReferral({
|
||||||
|
pubKey,
|
||||||
|
role: 'referee',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
data: referrer,
|
||||||
|
loading: referrerLoading,
|
||||||
|
error: referrerError,
|
||||||
|
} = useReferral({
|
||||||
|
pubKey,
|
||||||
|
role: 'referrer',
|
||||||
|
});
|
||||||
|
|
||||||
const showNav = !loading && !error && !data;
|
const error = refereeError || referrerError;
|
||||||
|
const loading = refereeLoading || referrerLoading;
|
||||||
|
const showNav = !loading && !error && !referrer && !referee;
|
||||||
|
|
||||||
usePageTitle(t('Referrals'));
|
usePageTitle(t('Referrals'));
|
||||||
|
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import { useReferrerStats } from './hooks/use-referrer-stats';
|
|
||||||
import {
|
|
||||||
BaseCommissionTile,
|
|
||||||
FinalCommissionTile,
|
|
||||||
RefereesTile,
|
|
||||||
StakingMultiplierTile,
|
|
||||||
TeamTile,
|
|
||||||
TotalCommissionTile,
|
|
||||||
VolumeTile,
|
|
||||||
dateFormatter,
|
|
||||||
} from './tiles';
|
|
||||||
import { CodeTile } from './tile';
|
|
||||||
|
|
||||||
export const ReferrerStatistics = ({
|
|
||||||
aggregationEpochs,
|
|
||||||
setId,
|
|
||||||
createdAt,
|
|
||||||
}: {
|
|
||||||
/** The aggregation epochs used to calculate statistics. */
|
|
||||||
aggregationEpochs: number;
|
|
||||||
/** The set id (code). */
|
|
||||||
setId: string;
|
|
||||||
/** The referral set date of creation. */
|
|
||||||
createdAt: string;
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
baseCommission,
|
|
||||||
finalCommission,
|
|
||||||
multiplier,
|
|
||||||
referees,
|
|
||||||
runningVolume,
|
|
||||||
totalCommission,
|
|
||||||
volume,
|
|
||||||
} = useReferrerStats(setId, aggregationEpochs);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-testid="referral-statistics"
|
|
||||||
data-as="referrer"
|
|
||||||
className="relative mx-auto mb-20"
|
|
||||||
>
|
|
||||||
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
|
|
||||||
{/** TEAM TILE - referral set id is the same as team id */}
|
|
||||||
<TeamTile teamId={setId} />
|
|
||||||
{/** TILES ROW 1 */}
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
||||||
<BaseCommissionTile
|
|
||||||
aggregationEpochs={aggregationEpochs}
|
|
||||||
baseCommission={baseCommission}
|
|
||||||
runningVolume={runningVolume}
|
|
||||||
/>
|
|
||||||
<StakingMultiplierTile multiplier={multiplier} />
|
|
||||||
<FinalCommissionTile
|
|
||||||
baseCommission={baseCommission}
|
|
||||||
multiplier={multiplier}
|
|
||||||
finalCommission={finalCommission}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/** TILES ROW 2 */}
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<CodeTile code={setId} createdAt={dateFormatter(createdAt)} />
|
|
||||||
<VolumeTile aggregationEpochs={aggregationEpochs} volume={volume} />
|
|
||||||
<RefereesTile referees={referees} />
|
|
||||||
<TotalCommissionTile
|
|
||||||
aggregationEpochs={aggregationEpochs}
|
|
||||||
totalCommission={totalCommission}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,440 +0,0 @@
|
|||||||
import { addDecimalsFormatNumber, getDateFormat } from '@vegaprotocol/utils';
|
|
||||||
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
|
||||||
import { useT } from '../../lib/use-t';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { type ReactNode } from 'react';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import { type StatValue, COMPACT_NUMBER_FORMAT } from './constants';
|
|
||||||
import { type ReferrerStats } from './hooks/use-referrer-stats';
|
|
||||||
import { type RefereeStats } from './hooks/use-referee-stats';
|
|
||||||
import { QUSDTooltip } from './qusd-tooltip';
|
|
||||||
import { NoProgramTile, StatTile, Tile } from './tile';
|
|
||||||
import { Loader, Tooltip } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { type BenefitTier } from './hooks/use-referral-program';
|
|
||||||
import { useTeam } from '../../lib/hooks/use-team';
|
|
||||||
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
|
|
||||||
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
|
||||||
import { TeamStats } from '../../components/competitions/team-stats';
|
|
||||||
|
|
||||||
/* Formatters */
|
|
||||||
|
|
||||||
const percentageFormatter = (value: BigNumber) =>
|
|
||||||
value.times(100).toFixed(2) + '%';
|
|
||||||
|
|
||||||
const compactFormatter =
|
|
||||||
(maximumFractionDigits = 2) =>
|
|
||||||
(value: BigNumber) =>
|
|
||||||
COMPACT_NUMBER_FORMAT(maximumFractionDigits).format(value.toNumber());
|
|
||||||
|
|
||||||
const valueFormatter = (noValueLabel: string) => (value: BigNumber) => {
|
|
||||||
if (value.isNaN() || value.isZero()) {
|
|
||||||
return noValueLabel;
|
|
||||||
}
|
|
||||||
return value.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dateFormatter = (value: string) => {
|
|
||||||
try {
|
|
||||||
return getDateFormat().format(new Date(value));
|
|
||||||
} catch {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Helpers */
|
|
||||||
|
|
||||||
const Value = <T,>({
|
|
||||||
data: { value, loading, error },
|
|
||||||
formatter,
|
|
||||||
}: {
|
|
||||||
data: StatValue<T>;
|
|
||||||
formatter: (value: T) => ReactNode;
|
|
||||||
}) => {
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<span className="p-[33px]">
|
|
||||||
<Loader size="small" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
return <span data-error={error.message}>-</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatter(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Referrer tiles */
|
|
||||||
|
|
||||||
export const BaseCommissionTile = ({
|
|
||||||
baseCommission,
|
|
||||||
runningVolume,
|
|
||||||
aggregationEpochs,
|
|
||||||
}: {
|
|
||||||
baseCommission: ReferrerStats['baseCommission'];
|
|
||||||
runningVolume: ReferrerStats['runningVolume'];
|
|
||||||
aggregationEpochs: number;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const runningVolumeDescription = compactFormatter(2)(runningVolume.value);
|
|
||||||
const description = t(
|
|
||||||
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
|
|
||||||
{
|
|
||||||
runningVolume: runningVolumeDescription,
|
|
||||||
epochs: aggregationEpochs.toString(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile
|
|
||||||
title={t('Base commission rate')}
|
|
||||||
description={description}
|
|
||||||
testId="base-commission-rate"
|
|
||||||
>
|
|
||||||
<Value data={baseCommission} formatter={percentageFormatter} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StakingMultiplierTile = ({
|
|
||||||
multiplier,
|
|
||||||
}: {
|
|
||||||
multiplier: ReferrerStats['multiplier'];
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
const { stakeAvailable, isEligible } = useStakeAvailable();
|
|
||||||
|
|
||||||
const description = (
|
|
||||||
<span
|
|
||||||
className={classNames({
|
|
||||||
'text-vega-red': !isEligible,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('{{amount}} $VEGA staked', {
|
|
||||||
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile
|
|
||||||
title={t('Staking multiplier')}
|
|
||||||
description={description}
|
|
||||||
testId="staking-multiplier"
|
|
||||||
>
|
|
||||||
<Value data={multiplier} formatter={valueFormatter(t('None'))} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FinalCommissionTile = ({
|
|
||||||
baseCommission,
|
|
||||||
multiplier,
|
|
||||||
finalCommission,
|
|
||||||
}: {
|
|
||||||
baseCommission: ReferrerStats['baseCommission'];
|
|
||||||
multiplier: ReferrerStats['multiplier'];
|
|
||||||
finalCommission: ReferrerStats['finalCommission'];
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const description =
|
|
||||||
!baseCommission.loading && !finalCommission.loading && !multiplier.loading
|
|
||||||
? `(${percentageFormatter(
|
|
||||||
baseCommission.value
|
|
||||||
)} × ${multiplier.value.toString()} = ${percentageFormatter(
|
|
||||||
finalCommission.value
|
|
||||||
)})`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile
|
|
||||||
title={t('Final commission rate')}
|
|
||||||
description={description}
|
|
||||||
testId="final-commission-rate"
|
|
||||||
>
|
|
||||||
<Value data={finalCommission} formatter={percentageFormatter} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VolumeTile = ({
|
|
||||||
volume,
|
|
||||||
aggregationEpochs,
|
|
||||||
}: {
|
|
||||||
volume: ReferrerStats['volume'];
|
|
||||||
aggregationEpochs: number;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile
|
|
||||||
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
|
||||||
count: aggregationEpochs,
|
|
||||||
})}
|
|
||||||
testId="my-volume"
|
|
||||||
>
|
|
||||||
<Value data={volume} formatter={compactFormatter(2)} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TotalCommissionTile = ({
|
|
||||||
totalCommission,
|
|
||||||
aggregationEpochs,
|
|
||||||
}: {
|
|
||||||
totalCommission: ReferrerStats['totalCommission'];
|
|
||||||
aggregationEpochs: number;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile
|
|
||||||
testId="total-commission"
|
|
||||||
title={
|
|
||||||
<Trans
|
|
||||||
i18nKey="totalCommission"
|
|
||||||
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
|
||||||
values={{
|
|
||||||
count: aggregationEpochs,
|
|
||||||
}}
|
|
||||||
components={[
|
|
||||||
<Tooltip
|
|
||||||
key="0"
|
|
||||||
description={t(
|
|
||||||
'Depending on data node retention you may not be able see the full 30 days'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>last 30 epochs</span>
|
|
||||||
</Tooltip>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
description={<QUSDTooltip />}
|
|
||||||
>
|
|
||||||
<Value data={totalCommission} formatter={compactFormatter(0)} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RefereesTile = ({
|
|
||||||
referees,
|
|
||||||
}: {
|
|
||||||
referees: ReferrerStats['referees'];
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
return (
|
|
||||||
<StatTile title={t('Number of traders')} testId="number-of-traders">
|
|
||||||
<Value data={referees} formatter={valueFormatter(t('None'))} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Referee tiles */
|
|
||||||
|
|
||||||
export const BenefitTierTile = ({
|
|
||||||
benefitTier,
|
|
||||||
nextBenefitTier,
|
|
||||||
}: {
|
|
||||||
benefitTier: RefereeStats['benefitTier'];
|
|
||||||
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const formatter = (value: BenefitTier | undefined) =>
|
|
||||||
value?.tier || t('None');
|
|
||||||
|
|
||||||
const next = nextBenefitTier.value?.tier;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile
|
|
||||||
title={t('Current tier')}
|
|
||||||
testId="current-tier"
|
|
||||||
description={
|
|
||||||
next
|
|
||||||
? t('(Next tier: {{nextTier}})', {
|
|
||||||
nextTier: next,
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Value<BenefitTier | undefined>
|
|
||||||
data={benefitTier}
|
|
||||||
formatter={formatter}
|
|
||||||
/>
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RunningVolumeTile = ({
|
|
||||||
runningVolume,
|
|
||||||
aggregationEpochs,
|
|
||||||
}: {
|
|
||||||
runningVolume: RefereeStats['runningVolume'];
|
|
||||||
aggregationEpochs: number;
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
return (
|
|
||||||
<StatTile
|
|
||||||
title={t(
|
|
||||||
'runningNotionalOverEpochs',
|
|
||||||
'Combined volume (last {{count}} epochs)',
|
|
||||||
{
|
|
||||||
count: aggregationEpochs,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
testId="combined-volume"
|
|
||||||
>
|
|
||||||
<Value data={runningVolume} formatter={compactFormatter(2)} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DiscountTile = ({
|
|
||||||
discountFactor,
|
|
||||||
}: {
|
|
||||||
discountFactor: RefereeStats['discountFactor'];
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
return (
|
|
||||||
<StatTile title={t('Discount')} testId="discount">
|
|
||||||
<Value data={discountFactor} formatter={percentageFormatter} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NextTierVolumeTile = ({
|
|
||||||
runningVolume,
|
|
||||||
nextBenefitTier,
|
|
||||||
}: {
|
|
||||||
runningVolume: RefereeStats['runningVolume'];
|
|
||||||
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
loading: runningVolume.loading || nextBenefitTier.loading,
|
|
||||||
error: runningVolume.error || nextBenefitTier.error,
|
|
||||||
value: [runningVolume.value, nextBenefitTier.value] as [
|
|
||||||
BigNumber,
|
|
||||||
BenefitTier | undefined
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatter = ([runningVolume, nextBenefitTier]: [
|
|
||||||
BigNumber,
|
|
||||||
BenefitTier | undefined
|
|
||||||
]) => {
|
|
||||||
if (!nextBenefitTier) return '0';
|
|
||||||
const volume = BigNumber(nextBenefitTier.minimumVolume).minus(
|
|
||||||
runningVolume
|
|
||||||
);
|
|
||||||
if (volume.isNaN() || volume.isLessThan(0)) return '0';
|
|
||||||
return compactFormatter(0)(volume);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile title={t('Volume to next tier')} testId="vol-to-next-tier">
|
|
||||||
<Value<[BigNumber, BenefitTier | undefined]>
|
|
||||||
data={data}
|
|
||||||
formatter={formatter}
|
|
||||||
/>
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EpochsTile = ({ epochs }: { epochs: RefereeStats['epochs'] }) => {
|
|
||||||
const t = useT();
|
|
||||||
return (
|
|
||||||
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
|
|
||||||
<Value data={epochs} formatter={valueFormatter(t('None'))} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NextTierEpochsTile = ({
|
|
||||||
epochs,
|
|
||||||
nextBenefitTier,
|
|
||||||
}: {
|
|
||||||
epochs: RefereeStats['epochs'];
|
|
||||||
nextBenefitTier: RefereeStats['nextBenefitTier'];
|
|
||||||
}) => {
|
|
||||||
const t = useT();
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
value: [epochs.value, nextBenefitTier.value] as [
|
|
||||||
BigNumber,
|
|
||||||
BenefitTier | undefined
|
|
||||||
],
|
|
||||||
loading: epochs.loading || nextBenefitTier.loading,
|
|
||||||
error: epochs.error || nextBenefitTier.error,
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatter = ([epochs, nextBenefitTier]: [
|
|
||||||
BigNumber,
|
|
||||||
BenefitTier | undefined
|
|
||||||
]) => {
|
|
||||||
if (!nextBenefitTier) return '-';
|
|
||||||
const value = BigNumber(nextBenefitTier.epochs).minus(epochs);
|
|
||||||
if (value.isLessThan(0)) {
|
|
||||||
return '0';
|
|
||||||
}
|
|
||||||
return value.toString(10);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StatTile title={t('Epochs to next tier')} testId="epochs-to-next-tier">
|
|
||||||
<Value data={data} formatter={formatter} />
|
|
||||||
</StatTile>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Additional settings */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A list for tiles that should be replaced with `NoProgramTile`
|
|
||||||
* when the referral program is not set.
|
|
||||||
*/
|
|
||||||
const NO_PROGRAM_TILES = {
|
|
||||||
[BaseCommissionTile.name]: 'Base commission rate',
|
|
||||||
[StakingMultiplierTile.name]: 'Staking multiplier',
|
|
||||||
[FinalCommissionTile.name]: 'Final commission rate',
|
|
||||||
[VolumeTile.name]: 'My volume',
|
|
||||||
[BenefitTierTile.name]: 'Current tier',
|
|
||||||
[DiscountTile.name]: 'Discount',
|
|
||||||
[RunningVolumeTile.name]: 'Combined volume',
|
|
||||||
[NextTierEpochsTile.name]: 'Epochs to next tier',
|
|
||||||
[NextTierVolumeTile.name]: 'Volume to next tier',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NoProgramTileFor = ({ tile }: { tile: string }) => {
|
|
||||||
const t = useT();
|
|
||||||
if (Object.keys(NO_PROGRAM_TILES).includes(tile)) {
|
|
||||||
return <NoProgramTile title={t(NO_PROGRAM_TILES[tile])} />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Teams */
|
|
||||||
|
|
||||||
export const TeamTile = ({ teamId }: { teamId?: string }) => {
|
|
||||||
const { team, members } = useTeam(teamId);
|
|
||||||
const { data: games } = useGames(teamId);
|
|
||||||
|
|
||||||
if (!team) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tile className="flex gap-3 lg:gap-4">
|
|
||||||
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
|
||||||
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
|
||||||
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
|
|
||||||
<TeamStats
|
|
||||||
members={members}
|
|
||||||
games={areTeamGames(games) ? games : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tile>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,63 +1,126 @@
|
|||||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||||
import {
|
import {
|
||||||
AppFailure,
|
|
||||||
AppLoader,
|
AppLoader,
|
||||||
NetworkLoader,
|
NetworkLoader,
|
||||||
NodeFailure,
|
|
||||||
NodeGuard,
|
|
||||||
useEnvironment,
|
useEnvironment,
|
||||||
|
useNodeSwitcherStore,
|
||||||
} from '@vegaprotocol/environment';
|
} from '@vegaprotocol/environment';
|
||||||
import { type ReactNode } from 'react';
|
import { useEffect, type ReactNode, useState } from 'react';
|
||||||
import { Web3Provider } from './web3-provider';
|
import { Web3Provider } from './web3-provider';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { DataLoader } from './data-loader';
|
import { DataLoader } from './data-loader';
|
||||||
import { WalletProvider } from '@vegaprotocol/wallet-react';
|
import { WalletProvider } from '@vegaprotocol/wallet-react';
|
||||||
import { useVegaWalletConfig } from '../../lib/hooks/use-vega-wallet-config';
|
import { useVegaWalletConfig } from '../../lib/hooks/use-vega-wallet-config';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import { Button, Loader, Splash, VLogo } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
const Failure = ({ reason }: { reason?: ReactNode }) => {
|
||||||
|
const t = useT();
|
||||||
|
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<div className="border border-vega-red m-10 mx-auto w-4/5 max-w-3xl rounded-lg overflow-hidden animate-shake">
|
||||||
|
<div className="bg-vega-red text-white px-2 py-2 flex gap-1 items-center font-alpha calt uppercase">
|
||||||
|
<VLogo className="h-4" />
|
||||||
|
<span className="text-lg">{t('Failed to initialize the app')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 text-left text-sm">
|
||||||
|
<p className="mb-4">{reason}</p>
|
||||||
|
<div className="text-center">
|
||||||
|
<Button className="border-2" onClick={() => setNodeSwitcher(true)}>
|
||||||
|
{t('Change node')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
const [showSlowNotification, setShowSlowNotification] = useState(false);
|
||||||
|
const t = useT();
|
||||||
|
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
|
||||||
|
useEffect(() => {
|
||||||
|
const to = setTimeout(() => {
|
||||||
|
if (!showSlowNotification) setShowSlowNotification(true);
|
||||||
|
}, 5000);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(to);
|
||||||
|
};
|
||||||
|
}, [showSlowNotification]);
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<div className="border border-transparent m-10 mx-auto w-4/5 max-w-3xl rounded-lg overflow-hidden">
|
||||||
|
<div className="mt-11 p-4 text-left text-sm">
|
||||||
|
<Loader />
|
||||||
|
{showSlowNotification && (
|
||||||
|
<>
|
||||||
|
<p className="mt-4 text-center">
|
||||||
|
{t(
|
||||||
|
"It looks like you're connection is slow, try switching to another node."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button
|
||||||
|
className="border-2"
|
||||||
|
onClick={() => setNodeSwitcher(true)}
|
||||||
|
>
|
||||||
|
{t('Change node')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { error, VEGA_URL } = useEnvironment();
|
|
||||||
|
const { error, VEGA_URL } = useEnvironment((state) => ({
|
||||||
|
error: state.error,
|
||||||
|
VEGA_URL: state.VEGA_URL,
|
||||||
|
}));
|
||||||
const config = useVegaWalletConfig();
|
const config = useVegaWalletConfig();
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <AppLoader />;
|
return <AppLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ERR_DATA_LOADER = (
|
||||||
|
<Trans
|
||||||
|
i18nKey="It appears that the connection to the node <0>{{VEGA_URL}}</0> does not return necessary data, try switching to another node."
|
||||||
|
components={[
|
||||||
|
<span key="vega" className="text-muted">
|
||||||
|
{VEGA_URL}
|
||||||
|
</span>,
|
||||||
|
]}
|
||||||
|
values={{
|
||||||
|
VEGA_URL,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkLoader
|
<NetworkLoader
|
||||||
cache={cacheConfig}
|
cache={cacheConfig}
|
||||||
skeleton={<AppLoader />}
|
skeleton={<Loading />}
|
||||||
failure={
|
failure={<Failure reason={error} />}
|
||||||
<AppFailure title={t('Could not initialize app')} error={error} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<NodeGuard
|
|
||||||
skeleton={<AppLoader />}
|
|
||||||
failure={
|
|
||||||
<NodeFailure
|
|
||||||
title={t('Node: {{VEGA_URL}} is unsuitable', { VEGA_URL })}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DataLoader
|
<DataLoader
|
||||||
skeleton={<AppLoader />}
|
skeleton={<Loading />}
|
||||||
failure={
|
failure={<Failure reason={ERR_DATA_LOADER} />}
|
||||||
<AppFailure
|
|
||||||
title={t('Could not load market data or asset data')}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Web3Provider
|
<Web3Provider
|
||||||
skeleton={<AppLoader />}
|
skeleton={<Loading />}
|
||||||
failure={
|
failure={<Failure reason={t('Could not configure web3 provider')} />}
|
||||||
<AppFailure title={t('Could not configure web3 provider')} />
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<WalletProvider config={config}>{children}</WalletProvider>
|
<WalletProvider config={config}>{children}</WalletProvider>
|
||||||
</Web3Provider>
|
</Web3Provider>
|
||||||
</DataLoader>
|
</DataLoader>
|
||||||
</NodeGuard>
|
|
||||||
</NetworkLoader>
|
</NetworkLoader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -107,6 +170,9 @@ const cacheConfig: InMemoryCacheConfig = {
|
|||||||
Fees: {
|
Fees: {
|
||||||
keyFields: false,
|
keyFields: false,
|
||||||
},
|
},
|
||||||
|
PartyProfile: {
|
||||||
|
keyFields: ['partyId'],
|
||||||
|
},
|
||||||
// The folling types are cached by the data provider and not by apollo
|
// The folling types are cached by the data provider and not by apollo
|
||||||
PositionUpdate: {
|
PositionUpdate: {
|
||||||
keyFields: false,
|
keyFields: false,
|
||||||
|
@ -30,8 +30,6 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
|||||||
setStudies,
|
setStudies,
|
||||||
setStudySizes,
|
setStudySizes,
|
||||||
setOverlays,
|
setOverlays,
|
||||||
state,
|
|
||||||
setState,
|
|
||||||
} = useChartSettings();
|
} = useChartSettings();
|
||||||
|
|
||||||
const pennantChart = (
|
const pennantChart = (
|
||||||
@ -68,10 +66,6 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
|||||||
onIntervalChange={(newInterval) => {
|
onIntervalChange={(newInterval) => {
|
||||||
setInterval(fromTradingViewResolution(newInterval));
|
setInterval(fromTradingViewResolution(newInterval));
|
||||||
}}
|
}}
|
||||||
onAutoSaveNeeded={(data) => {
|
|
||||||
setState(data);
|
|
||||||
}}
|
|
||||||
state={state}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ type StudySizes = { [S in Study]?: number };
|
|||||||
export type Chartlib = 'pennant' | 'tradingview';
|
export type Chartlib = 'pennant' | 'tradingview';
|
||||||
|
|
||||||
interface StoredSettings {
|
interface StoredSettings {
|
||||||
state: object | undefined; // Don't see a better type provided from TradingView type definitions
|
|
||||||
chartlib: Chartlib;
|
chartlib: Chartlib;
|
||||||
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
|
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
|
||||||
// chart types easier and more consistent
|
// chart types easier and more consistent
|
||||||
@ -30,7 +29,6 @@ const STUDY_ORDER: Study[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_CHART_SETTINGS = {
|
export const DEFAULT_CHART_SETTINGS = {
|
||||||
state: undefined,
|
|
||||||
chartlib: 'pennant' as const,
|
chartlib: 'pennant' as const,
|
||||||
interval: Interval.INTERVAL_I15M,
|
interval: Interval.INTERVAL_I15M,
|
||||||
type: ChartType.CANDLE,
|
type: ChartType.CANDLE,
|
||||||
@ -47,7 +45,6 @@ export const useChartSettingsStore = create<
|
|||||||
setStudies: (studies?: Study[]) => void;
|
setStudies: (studies?: Study[]) => void;
|
||||||
setStudySizes: (sizes: number[]) => void;
|
setStudySizes: (sizes: number[]) => void;
|
||||||
setChartlib: (lib: Chartlib) => void;
|
setChartlib: (lib: Chartlib) => void;
|
||||||
setState: (state: object) => void;
|
|
||||||
}
|
}
|
||||||
>()(
|
>()(
|
||||||
persist(
|
persist(
|
||||||
@ -95,9 +92,6 @@ export const useChartSettingsStore = create<
|
|||||||
state.chartlib = lib;
|
state.chartlib = lib;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setState: (state) => {
|
|
||||||
set({ state });
|
|
||||||
},
|
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: 'vega_candles_chart_store',
|
name: 'vega_candles_chart_store',
|
||||||
@ -151,7 +145,5 @@ export const useChartSettings = () => {
|
|||||||
setOverlays: settings.setOverlays,
|
setOverlays: settings.setOverlays,
|
||||||
setStudySizes: settings.setStudySizes,
|
setStudySizes: settings.setStudySizes,
|
||||||
setChartlib: settings.setChartlib,
|
setChartlib: settings.setChartlib,
|
||||||
state: settings.state,
|
|
||||||
setState: settings.setState,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,12 @@ import {
|
|||||||
mockConfig,
|
mockConfig,
|
||||||
MockedWalletProvider,
|
MockedWalletProvider,
|
||||||
} from '@vegaprotocol/wallet-react/testing';
|
} from '@vegaprotocol/wallet-react/testing';
|
||||||
|
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
|
||||||
|
import {
|
||||||
|
PartyProfilesDocument,
|
||||||
|
type PartyProfilesQuery,
|
||||||
|
type PartyProfilesQueryVariables,
|
||||||
|
} from '../vega-wallet-connect-button/__generated__/PartyProfiles';
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/proposals', () => ({
|
jest.mock('@vegaprotocol/proposals', () => ({
|
||||||
ProtocolUpgradeCountdown: () => null,
|
ProtocolUpgradeCountdown: () => null,
|
||||||
@ -24,15 +30,45 @@ describe('Navbar', () => {
|
|||||||
publicKey: '2'.repeat(64),
|
publicKey: '2'.repeat(64),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const key1Alias = 'key 1 alias';
|
||||||
const marketId = 'abc';
|
const marketId = 'abc';
|
||||||
const navbarContent = 'navbar-menu-content';
|
const navbarContent = 'navbar-menu-content';
|
||||||
|
|
||||||
|
const partyProfilesMock: MockedResponse<
|
||||||
|
PartyProfilesQuery,
|
||||||
|
PartyProfilesQueryVariables
|
||||||
|
> = {
|
||||||
|
request: {
|
||||||
|
query: PartyProfilesDocument,
|
||||||
|
variables: {
|
||||||
|
partyIds: mockKeys.map((k) => k.publicKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
partiesProfilesConnection: {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
partyId: mockKeys[0].publicKey,
|
||||||
|
alias: key1Alias,
|
||||||
|
metadata: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const renderComponent = (initialEntries?: string[]) => {
|
const renderComponent = (initialEntries?: string[]) => {
|
||||||
return render(
|
return render(
|
||||||
<MemoryRouter initialEntries={initialEntries}>
|
<MemoryRouter initialEntries={initialEntries}>
|
||||||
|
<MockedProvider mocks={[partyProfilesMock]}>
|
||||||
<MockedWalletProvider>
|
<MockedWalletProvider>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</MockedWalletProvider>
|
</MockedWalletProvider>
|
||||||
|
</MockedProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -140,6 +176,7 @@ describe('Navbar', () => {
|
|||||||
const activeKey = within(menu.getByTestId(/key-1+-mobile/));
|
const activeKey = within(menu.getByTestId(/key-1+-mobile/));
|
||||||
expect(activeKey.getByText(mockKeys[0].name)).toBeInTheDocument();
|
expect(activeKey.getByText(mockKeys[0].name)).toBeInTheDocument();
|
||||||
expect(activeKey.getByTestId('icon-tick')).toBeInTheDocument();
|
expect(activeKey.getByTestId('icon-tick')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(key1Alias)).toBeInTheDocument();
|
||||||
|
|
||||||
const inactiveKey = within(menu.getByTestId(/key-2+-mobile/));
|
const inactiveKey = within(menu.getByTestId(/key-2+-mobile/));
|
||||||
await userEvent.click(inactiveKey.getByText(mockKeys[1].name));
|
await userEvent.click(inactiveKey.getByText(mockKeys[1].name));
|
||||||
|
@ -31,39 +31,27 @@ export const ProfileDialog = () => {
|
|||||||
const pubKey = useProfileDialogStore((store) => store.pubKey);
|
const pubKey = useProfileDialogStore((store) => store.pubKey);
|
||||||
const setOpen = useProfileDialogStore((store) => store.setOpen);
|
const setOpen = useProfileDialogStore((store) => store.setOpen);
|
||||||
|
|
||||||
const { send, status, error, reset } = useSimpleTransaction({
|
|
||||||
onSuccess: () => {
|
|
||||||
refetch();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const profileEdge = data?.partiesProfilesConnection?.edges.find(
|
const profileEdge = data?.partiesProfilesConnection?.edges.find(
|
||||||
(e) => e.node.partyId === pubKey
|
(e) => e.node.partyId === pubKey
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendTx = (field: FormFields) => {
|
|
||||||
send({
|
|
||||||
updatePartyProfile: {
|
|
||||||
alias: field.alias,
|
|
||||||
metadata: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setOpen(undefined);
|
setOpen(undefined);
|
||||||
reset();
|
|
||||||
}}
|
}}
|
||||||
title={t('Edit profile')}
|
title={t('Edit profile')}
|
||||||
>
|
>
|
||||||
<ProfileForm
|
<ProfileFormContainer
|
||||||
profile={profileEdge?.node}
|
profile={profileEdge?.node}
|
||||||
status={status}
|
onSuccess={() => {
|
||||||
error={error}
|
refetch();
|
||||||
onSubmit={sendTx}
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpen(undefined);
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@ -77,6 +65,32 @@ type Profile = NonNullable<
|
|||||||
PartyProfilesQuery['partiesProfilesConnection']
|
PartyProfilesQuery['partiesProfilesConnection']
|
||||||
>['edges'][number]['node'];
|
>['edges'][number]['node'];
|
||||||
|
|
||||||
|
const ProfileFormContainer = ({
|
||||||
|
profile,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
profile: Profile | undefined;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}) => {
|
||||||
|
const { send, status, error } = useSimpleTransaction({ onSuccess });
|
||||||
|
const sendTx = (field: FormFields) => {
|
||||||
|
send({
|
||||||
|
updatePartyProfile: {
|
||||||
|
alias: field.alias,
|
||||||
|
metadata: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ProfileForm
|
||||||
|
profile={profile}
|
||||||
|
status={status}
|
||||||
|
error={error}
|
||||||
|
onSubmit={sendTx}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ProfileForm = ({
|
const ProfileForm = ({
|
||||||
profile,
|
profile,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -114,6 +128,14 @@ const ProfileForm = ({
|
|||||||
|
|
||||||
const errorMessage = errors.alias?.message || error;
|
const errorMessage = errors.alias?.message || error;
|
||||||
|
|
||||||
|
if (status === 'confirmed') {
|
||||||
|
return (
|
||||||
|
<p className="mt-2 mb-4 text-sm text-vega-green-600 dark:text-vega-green">
|
||||||
|
{t('Profile updated')}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
|
||||||
<FormGroup label="Alias" labelFor="alias">
|
<FormGroup label="Alias" labelFor="alias">
|
||||||
@ -131,12 +153,6 @@ const ProfileForm = ({
|
|||||||
</p>
|
</p>
|
||||||
</InputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status === 'confirmed' && (
|
|
||||||
<p className="mt-2 mb-4 text-sm text-success">
|
|
||||||
{t('Profile updated')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<TradingButton
|
<TradingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -94,6 +94,7 @@ export const VegaWalletConnectButton = ({
|
|||||||
<KeypairRadioGroup
|
<KeypairRadioGroup
|
||||||
pubKey={pubKey}
|
pubKey={pubKey}
|
||||||
pubKeys={pubKeys}
|
pubKeys={pubKeys}
|
||||||
|
activeKey={activeKey?.publicKey}
|
||||||
onSelect={selectPubKey}
|
onSelect={selectPubKey}
|
||||||
/>
|
/>
|
||||||
<TradingDropdownSeparator />
|
<TradingDropdownSeparator />
|
||||||
@ -138,15 +139,18 @@ export const VegaWalletConnectButton = ({
|
|||||||
const KeypairRadioGroup = ({
|
const KeypairRadioGroup = ({
|
||||||
pubKey,
|
pubKey,
|
||||||
pubKeys,
|
pubKeys,
|
||||||
|
activeKey,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
pubKey: string | undefined;
|
pubKey: string | undefined;
|
||||||
pubKeys: Key[];
|
pubKeys: Key[];
|
||||||
|
activeKey: string | undefined;
|
||||||
onSelect: (pubKey: string) => void;
|
onSelect: (pubKey: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = usePartyProfilesQuery({
|
const { data } = usePartyProfilesQuery({
|
||||||
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
|
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
|
||||||
skip: pubKeys.length <= 0,
|
skip: pubKeys.length <= 0,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -156,14 +160,27 @@ const KeypairRadioGroup = ({
|
|||||||
(e) => e.node.partyId === pk.publicKey
|
(e) => e.node.partyId === pk.publicKey
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<KeypairItem key={pk.publicKey} pk={pk} alias={profile?.node.alias} />
|
<KeypairItem
|
||||||
|
key={pk.publicKey}
|
||||||
|
pk={pk}
|
||||||
|
isActive={activeKey === pk.publicKey}
|
||||||
|
alias={profile?.node.alias}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TradingDropdownRadioGroup>
|
</TradingDropdownRadioGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const KeypairItem = ({ pk, alias }: { pk: Key; alias: string | undefined }) => {
|
const KeypairItem = ({
|
||||||
|
pk,
|
||||||
|
isActive,
|
||||||
|
alias,
|
||||||
|
}: {
|
||||||
|
pk: Key;
|
||||||
|
alias: string | undefined;
|
||||||
|
isActive: boolean;
|
||||||
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const [copied, setCopied] = useCopyTimeout();
|
const [copied, setCopied] = useCopyTimeout();
|
||||||
const setOpen = useProfileDialogStore((store) => store.setOpen);
|
const setOpen = useProfileDialogStore((store) => store.setOpen);
|
||||||
@ -194,8 +211,13 @@ const KeypairItem = ({ pk, alias }: { pk: Key; alias: string | undefined }) => {
|
|||||||
data-testid={`key-${pk.publicKey}`}
|
data-testid={`key-${pk.publicKey}`}
|
||||||
>
|
>
|
||||||
<Tooltip description={t('Public facing key alias. Click to edit')}>
|
<Tooltip description={t('Public facing key alias. Click to edit')}>
|
||||||
<button data-testid="alias" onClick={() => setOpen(pk.publicKey)}>
|
<button
|
||||||
|
data-testid="alias"
|
||||||
|
onClick={() => setOpen(pk.publicKey)}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
{alias ? alias : t('No alias')}
|
{alias ? alias : t('No alias')}
|
||||||
|
{isActive && <VegaIcon name={VegaIconNames.EDIT} />}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,8 @@ import CopyToClipboard from 'react-copy-to-clipboard';
|
|||||||
import { ViewType, useSidebar } from '../sidebar';
|
import { ViewType, useSidebar } from '../sidebar';
|
||||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { usePartyProfilesQuery } from '../vega-wallet-connect-button/__generated__/PartyProfiles';
|
||||||
|
import { useProfileDialogStore } from '../../stores/profile-dialog-store';
|
||||||
|
|
||||||
export const VegaWalletMenu = ({
|
export const VegaWalletMenu = ({
|
||||||
setMenu,
|
setMenu,
|
||||||
@ -23,6 +25,12 @@ export const VegaWalletMenu = ({
|
|||||||
const currentRouteId = useGetCurrentRouteId();
|
const currentRouteId = useGetCurrentRouteId();
|
||||||
const setViews = useSidebar((store) => store.setViews);
|
const setViews = useSidebar((store) => store.setViews);
|
||||||
|
|
||||||
|
const { data } = usePartyProfilesQuery({
|
||||||
|
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
|
||||||
|
skip: pubKeys.length <= 0,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
const activeKey = useMemo(() => {
|
const activeKey = useMemo(() => {
|
||||||
return pubKeys?.find((pk) => pk.publicKey === pubKey);
|
return pubKeys?.find((pk) => pk.publicKey === pubKey);
|
||||||
}, [pubKey, pubKeys]);
|
}, [pubKey, pubKeys]);
|
||||||
@ -37,14 +45,21 @@ export const VegaWalletMenu = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="grow my-4" role="list">
|
<div className="grow my-4" role="list">
|
||||||
{(pubKeys || []).map((pk) => (
|
{(pubKeys || []).map((pk) => {
|
||||||
|
const profile = data?.partiesProfilesConnection?.edges.find(
|
||||||
|
(e) => e.node.partyId === pk.publicKey
|
||||||
|
);
|
||||||
|
return (
|
||||||
<KeypairListItem
|
<KeypairListItem
|
||||||
key={pk.publicKey}
|
key={pk.publicKey}
|
||||||
pk={pk}
|
pk={pk}
|
||||||
isActive={activeKey?.publicKey === pk.publicKey}
|
isActive={activeKey?.publicKey === pk.publicKey}
|
||||||
onSelectItem={onSelectItem}
|
onSelectItem={onSelectItem}
|
||||||
|
alias={profile?.node.alias}
|
||||||
|
setMenu={setMenu}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 m-4">
|
<div className="flex flex-col gap-2 m-4">
|
||||||
@ -72,18 +87,23 @@ export const VegaWalletMenu = ({
|
|||||||
const KeypairListItem = ({
|
const KeypairListItem = ({
|
||||||
pk,
|
pk,
|
||||||
isActive,
|
isActive,
|
||||||
|
alias,
|
||||||
onSelectItem,
|
onSelectItem,
|
||||||
|
setMenu,
|
||||||
}: {
|
}: {
|
||||||
pk: Key;
|
pk: Key;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
alias: string | undefined;
|
||||||
onSelectItem: (pk: string) => void;
|
onSelectItem: (pk: string) => void;
|
||||||
|
setMenu: (open: 'nav' | 'wallet' | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const [copied, setCopied] = useCopyTimeout();
|
const [copied, setCopied] = useCopyTimeout();
|
||||||
|
const setOpen = useProfileDialogStore((store) => store.setOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col w-full ml-4 mr-2 mb-4"
|
className="flex flex-col w-full px-4 mb-4"
|
||||||
data-testid={`key-${pk.publicKey}-mobile`}
|
data-testid={`key-${pk.publicKey}-mobile`}
|
||||||
>
|
>
|
||||||
<span className="flex gap-2 items-center mr-2">
|
<span className="flex gap-2 items-center mr-2">
|
||||||
@ -106,6 +126,24 @@ const KeypairListItem = ({
|
|||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
{copied && <span className="text-xs">{t('Copied')}</span>}
|
{copied && <span className="text-xs">{t('Copied')}</span>}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
data-testid={`key-${pk.publicKey}`}
|
||||||
|
>
|
||||||
|
<span className="truncate">{alias ? alias : t('No alias')}</span>
|
||||||
|
{isActive && (
|
||||||
|
<button
|
||||||
|
data-testid="alias"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(pk.publicKey);
|
||||||
|
setMenu(null);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<VegaIcon name={VegaIconNames.EDIT} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -50,8 +50,6 @@ def truncate_middle(market_id, start=6, end=4):
|
|||||||
def change_keys(page: Page, vega: VegaServiceNull, key_name):
|
def change_keys(page: Page, vega: VegaServiceNull, key_name):
|
||||||
page.get_by_test_id("manage-vega-wallet").click()
|
page.get_by_test_id("manage-vega-wallet").click()
|
||||||
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
|
page.get_by_test_id("key-" + vega.wallet.public_key(key_name)).click()
|
||||||
page.click(
|
|
||||||
f'data-testid=key-{vega.wallet.public_key(key_name)} >> .inline-flex')
|
|
||||||
page.reload()
|
page.reload()
|
||||||
|
|
||||||
|
|
||||||
|
@ -339,4 +339,3 @@ def retry_on_http_error(request):
|
|||||||
print(f"Retrying due to HTTPError (attempt {i+1}/{retry_count})")
|
print(f"Retrying due to HTTPError (attempt {i+1}/{retry_count})")
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user