Compare commits

..

1 Commits

Author SHA1 Message Date
asiaznik
f93ac8d0c6
feat(trading): referrals upgrades 2024-03-08 19:24:49 +01:00
253 changed files with 5261 additions and 3912 deletions

View File

@ -4,5 +4,6 @@ tmp/*
.dockerignore
dockerfiles
node_modules
.git
.github
.vscode

View File

@ -196,9 +196,9 @@ jobs:
cypress:
needs: [build-sources, check-e2e-needed]
name: '(CI) cypress'
if: ${{ needs.check-e2e-needed.outputs.run-tests == 'true' }}
uses: ./.github/workflows/cypress-run.yml
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:
projects: ${{ needs.build-sources.outputs.projects-e2e }}
tags: '@smoke'
@ -287,9 +287,8 @@ jobs:
steps:
- run: |
result="${{ needs.cypress.result }}"
echo "Result: $result"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
exit 0
fi

View File

@ -1,28 +0,0 @@
# 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

View File

@ -1,9 +1,7 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const { theme } = require('../../libs/tailwindcss-config/src/theme');
const {
vegaCustomClasses,
} = require('../../libs/tailwindcss-config/src/vega-custom-classes');
const theme = require('../../libs/tailwindcss-config/src/theme');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
module.exports = {
content: [

View File

@ -153,7 +153,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
.invoke('text')
.should('not.eq', currentBlockHeight);
});
cy.getByTestId('subscription-cell').should('be.be.visible');
cy.getByTestId('subscription-cell').should('have.text', 'Yes');
});
cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('node-url-custom').click({ force: true });

View File

@ -26,9 +26,9 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
const { token, staking, vesting } = useContracts();
const setAssociatedBalances = useRefreshAssociatedBalances();
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
const vegaWalletStatus = useEagerConnect();
const vegaConnecting = useEagerConnect();
const loaded = balancesLoaded && vegaWalletStatus !== 'connecting';
const loaded = balancesLoaded && !vegaConnecting;
React.useEffect(() => {
const run = async () => {
@ -169,5 +169,3 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
}
return <Suspense fallback={loading}>{children}</Suspense>;
};
AppLoader.displayName = 'AppLoader';

View File

@ -111,4 +111,3 @@ export const ContractsProvider = ({ children }: { children: JSX.Element }) => {
</ContractsContext.Provider>
);
};
ContractsProvider.displayName = 'ContractsProvider';

View File

@ -38,7 +38,6 @@ import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import { MarketName } from '../proposal/market-name';
import { Indicator } from '../proposal/indicator';
import { type ProposalNode } from '../proposal/proposal-utils';
const ProposalTypeTags = ({
proposal,
@ -541,12 +540,10 @@ const BatchProposalStateText = ({
export const ProposalHeader = ({
proposal,
restData,
isListItem = true,
voteState,
}: {
proposal: Proposal | BatchProposal;
restData?: ProposalNode | null;
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
@ -598,7 +595,7 @@ export const ProposalHeader = ({
)}
</div>
<ProposalDetails proposal={proposal} />
<VoteBreakdown proposal={proposal} restData={restData} />
<VoteBreakdown proposal={proposal} />
</>
);
};

View File

@ -91,28 +91,6 @@ export type ProposalNode = {
proposal: ProposalData;
proposalType: ProposalNodeType;
proposals: SubProposalData[];
yes?: [
{
partyId: string;
elsPerMarket?: [
{
marketId: string;
els: string;
}
];
}
];
no?: [
{
partyId: string;
elsPerMarket?: [
{
marketId: string;
els: string;
}
];
}
];
};
type SingleProposalNode = ProposalNode & {

View File

@ -48,7 +48,6 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
<ProposalHeader
proposal={proposal}
restData={restData}
isListItem={false}
voteState={voteState}
/>

View File

@ -17,7 +17,6 @@ import {
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
import { MarketName } from '../proposal/market-name';
import { Indicator } from '../proposal/indicator';
import { type ProposalNode } from '../proposal/proposal-utils';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
@ -111,64 +110,24 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
export const VoteBreakdown = ({
proposal,
restData,
}: {
proposal: Proposal | BatchProposal;
restData?: ProposalNode | null;
}) => {
if (proposal.__typename === 'Proposal') {
return <VoteBreakdownNormal proposal={proposal} />;
}
if (proposal.__typename === 'BatchProposal') {
return <VoteBreakdownBatch proposal={proposal} restData={restData} />;
return <VoteBreakdownBatch proposal={proposal} />;
}
return null;
};
const VoteBreakdownBatch = ({
proposal,
restData,
}: {
proposal: BatchProposal;
restData?: ProposalNode | null;
}) => {
const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
const [fullBreakdown, setFullBreakdown] = useState(false);
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({
terms: compact(
proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : []
@ -235,8 +194,6 @@ const VoteBreakdownBatch = ({
proposal={proposal}
votes={proposal.votes}
terms={p.terms}
yesELS={yesELS}
noELS={noELS}
/>
);
})}
@ -297,8 +254,6 @@ const VoteBreakdownBatch = ({
proposal={proposal}
votes={proposal.votes}
terms={p.terms}
yesELS={yesELS}
noELS={noELS}
/>
);
})}
@ -316,17 +271,17 @@ const VoteBreakdownBatchSubProposal = ({
votes,
terms,
indicator,
yesELS,
noELS,
}: {
proposal: BatchProposal;
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment;
indicator?: number;
yesELS: Record<string, number[]>;
noELS: Record<string, number[]>;
}) => {
const { t } = useTranslation();
const voteInfo = useVoteInformation({
votes,
terms,
});
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
@ -339,15 +294,6 @@ const VoteBreakdownBatchSubProposal = ({
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 ? (
<>
: <MarketName marketId={marketId} />

View File

@ -8,18 +8,13 @@ import {
type VoteFieldsFragment,
} from '../__generated__/Proposals';
import { type ProposalChangeType } from '../types';
import sum from 'lodash/sum';
export const useVoteInformation = ({
votes,
terms,
yesELS,
noELS,
}: {
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment;
yesELS?: number[];
noELS?: number[];
}) => {
const {
appState: { totalSupply, decimals },
@ -36,9 +31,7 @@ export const useVoteInformation = ({
paramsForChange,
votes,
totalSupply,
decimals,
yesELS,
noELS
decimals
);
};
@ -79,11 +72,7 @@ const getVoteData = (
},
votes: ProposalFieldsFragment['votes'],
totalSupply: BigNumber,
decimals: number,
/** A list of ELS yes votes */
yesELS?: number[],
/** A list if ELS no votes */
noELS?: number[]
decimals: number
) => {
const requiredMajorityPercentage = params.requiredMajority
? new BigNumber(params.requiredMajority).times(100)
@ -97,31 +86,17 @@ const getVoteData = (
addDecimal(votes.no.totalTokens ?? 0, decimals)
);
let noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
const noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
? new BigNumber(0)
: 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(
addDecimal(votes.yes.totalTokens ?? 0, decimals)
);
let yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
const yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
? new BigNumber(0)
: 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);

View File

@ -1,11 +1,9 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const { theme } = require('../../libs/tailwindcss-config/src/theme');
const {
vegaCustomClasses,
} = require('../../libs/tailwindcss-config/src/vega-custom-classes');
const theme = require('../../libs/tailwindcss-config/src/theme');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
export default {
module.exports = {
content: [
join(__dirname, 'src/**/*.{js,ts,jsx,tsx}'),
'libs/ui-toolkit/src/utils/shared.ts',

View File

@ -0,0 +1,11 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,16 @@
# 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'.

View File

@ -0,0 +1,28 @@
# 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

View File

@ -0,0 +1,3 @@
# App configuration variables
NX_VEGA_URL=http://localhost:3008/graphql
NX_VEGA_ENV=LOCAL

View File

@ -0,0 +1,8 @@
# 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=#

View File

@ -0,0 +1,9 @@
# 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

View File

@ -0,0 +1,9 @@
# 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\"}

View File

@ -0,0 +1,9 @@
# 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

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,11 @@
/* 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',
};

View File

@ -0,0 +1,10 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

View File

@ -0,0 +1,94 @@
{
"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": []
}

View File

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

View File

@ -0,0 +1,25 @@
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>
</>
);
}

View File

@ -0,0 +1 @@
export * from './dashboard';

View File

@ -0,0 +1 @@
export * from './intro';

View File

@ -0,0 +1,53 @@
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>
);
};

View File

@ -0,0 +1 @@
export * from './market-list';

View File

@ -0,0 +1,296 @@
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}`);

View File

@ -0,0 +1,103 @@
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>
);
};

View File

@ -0,0 +1,26 @@
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>
);
};

View File

@ -0,0 +1 @@
export * from './header';

View File

@ -0,0 +1 @@
export * from './detail';

View File

@ -0,0 +1 @@
export * from './last-24h-volume';

View File

@ -0,0 +1,108 @@
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>
);
};

View File

@ -0,0 +1 @@
export * from './market';

View File

@ -0,0 +1,118 @@
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>
);
};

View File

@ -0,0 +1 @@
export * from './providers';

View File

@ -0,0 +1,125 @@
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}
/>
);
};

View File

@ -0,0 +1,50 @@
.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;
}

View File

@ -0,0 +1,47 @@
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}
/>
);
};

View File

@ -0,0 +1 @@
export * from './grid';

View File

@ -0,0 +1,110 @@
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>
);
};

View File

@ -0,0 +1 @@
export * from './health-dialog';

View File

@ -0,0 +1 @@
export * from './indicator';

View File

@ -0,0 +1,24 @@
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>
);
};

View File

@ -0,0 +1,35 @@
.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;
}

View File

@ -0,0 +1 @@
export * from './navbar';

View File

@ -0,0 +1,15 @@
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>
);
};

View File

@ -0,0 +1 @@
export * from './status';

View File

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

View File

@ -0,0 +1,28 @@
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];
};

View File

@ -0,0 +1 @@
export * from './router-config';

View File

@ -0,0 +1,25 @@
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,
},
];

View File

@ -0,0 +1,3 @@
export const environment = {
production: true,
};

View File

@ -0,0 +1,6 @@
// 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.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,23 @@
<!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>

View File

@ -0,0 +1,15 @@
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>
);

View File

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

View File

@ -0,0 +1,10 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
@apply h-full;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
}

View File

@ -0,0 +1,29 @@
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],
};

View File

@ -0,0 +1,27 @@
{
"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"]
}

View File

@ -0,0 +1,24 @@
{
"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"
}
]
}

View File

@ -0,0 +1,33 @@
{
"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"
]
}

View File

@ -1,9 +1,7 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const { theme } = require('../../libs/tailwindcss-config/src/theme');
const {
vegaCustomClasses,
} = require('../../libs/tailwindcss-config/src/vega-custom-classes');
const theme = require('../../libs/tailwindcss-config/src/theme');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
module.exports = {
content: [

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,22 @@
{
"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"
}
]
}

View File

@ -16,9 +16,7 @@ import {
useVegaWallet,
useDialogStore,
} from '@vegaprotocol/wallet-react';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links';
import { Statistics, useStats } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program';
import { ns, useT } from '../../lib/use-t';
import { useFundsAvailable } from './hooks/use-funds-available';
@ -26,6 +24,12 @@ import { ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { QUSDTooltip } from './qusd-tooltip';
import { Trans } from 'react-i18next';
import { PreviewRefereeStatistics } from './referee-statistics';
import {
useReferralSet,
useIsInReferralSet,
} from './hooks/use-find-referral-set';
import minBy from 'lodash/minBy';
const RELOAD_DELAY = 3000;
@ -106,9 +110,11 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined,
});
const {
data: previewData,
loading: previewLoading,
isEligible: isPreviewEligible,
} = useReferralSet(validateCode(codeField, t) ? codeField : undefined);
const { send, status } = useSimpleTransaction({
onSuccess: () => {
@ -141,19 +147,14 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
* Validates the set a user tries to apply to.
*/
const validateSet = useCallback(() => {
if (
codeField &&
!previewLoading &&
previewData &&
!previewData.isEligible
) {
if (codeField && !previewLoading && previewData && !isPreviewEligible) {
return t('The code is no longer valid.');
}
if (codeField && !previewLoading && !previewData) {
return t('The code is invalid');
}
return true;
}, [codeField, previewData, previewLoading, t]);
}, [codeField, isPreviewEligible, previewData, previewLoading, t]);
const noFunds = validateFundsAvailable() !== true ? true : false;
@ -200,8 +201,6 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
// });
};
const { epochsValue, nextBenefitTierValue } = useStats({ program });
// show "code applied" message when successfully applied
if (status === 'confirmed') {
return (
@ -264,9 +263,10 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
};
};
const nextBenefitTierEpochsValue = nextBenefitTierValue
? nextBenefitTierValue.epochs - epochsValue
: 0;
// calculate minimum amount of epochs a referee has to be in a set in order
// to benefit from it
const firstBenefitTier = minBy(program.benefitTiers, (bt) => bt.epochs);
const minEpochs = firstBenefitTier ? firstBenefitTier.epochs : 0;
return (
<>
@ -335,17 +335,17 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
<Loader />
</div>
) : null}
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData && previewData.isEligible ? (
{previewData && isPreviewEligible ? (
<div className="mt-10">
<h2 className="mb-5 text-2xl">
{t(
'youAreJoiningTheGroup',
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
{ count: nextBenefitTierEpochsValue }
{ count: minEpochs }
)}
</h2>
<Statistics data={previewData} program={program} as="referee" />
<PreviewRefereeStatistics setId={codeField} />
</div>
) : null}
</>

View File

@ -1,3 +1,6 @@
import { type ApolloError } from '@apollo/client';
import { getUserLocale } from '@vegaprotocol/utils';
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
@ -8,3 +11,19 @@ export const REFERRAL_DOCS_LINK =
export const ABOUT_REFERRAL_DOCS_LINK =
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
export const DISCLAIMER_REFERRAL_DOCS_LINK = 'https://docs.vega.xyz/';
export const DEFAULT_AGGREGATION_DAYS = 30;
export type StatValue<T> = {
value: T;
loading: boolean;
error?: ApolloError | Error;
};
export const COMPACT_NUMBER_FORMAT = (maximumFractionDigits = 2) =>
new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: 0,
maximumFractionDigits,
notation: 'compact',
compactDisplay: 'short',
});

View File

@ -16,13 +16,16 @@ import {
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { useT } from '../../lib/use-t';
import { Link, Navigate, useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../lib/links';
import { useReferralProgram } from './hooks/use-referral-program';
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
import { Trans } from 'react-i18next';
import {
useFindReferralSet,
useIsInReferralSet,
} from './hooks/use-find-referral-set';
export const CreateCodeContainer = () => {
const t = useT();
@ -145,7 +148,7 @@ const CreateCodeDialog = ({
const t = useT();
const createLink = useLinks(DApp.Governance);
const { pubKey } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' });
const { refetch } = useFindReferralSet(pubKey);
const {
err,
code,

View File

@ -0,0 +1,122 @@
import { useCallback } from 'react';
import {
type ReferralSetsQueryVariables,
useReferralSetsQuery,
} from './__generated__/ReferralSets';
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
export type Role = 'referrer' | 'referee';
type Args = (
| { setId: string | undefined }
| { pubKey: string | undefined; role: Role }
) & {
aggregationEpochs?: number;
};
export const prepareVariables = (
args: Args
): [ReferralSetsQueryVariables, boolean] => {
const byId = 'setId' in args;
const byRole = 'pubKey' in args && 'role' in args;
let variables = {};
let skip = true;
if (byId) {
variables = {
id: args.setId,
};
skip = !args.setId;
}
if (byRole) {
if (args.role === 'referee') {
variables = { referee: args.pubKey };
}
if (args.role === 'referrer') {
variables = { referrer: args.pubKey };
}
skip = !args.pubKey;
}
return [variables, skip];
};
export const useFindReferralSet = (pubKey?: string) => {
const [referrerVariables, referrerSkip] = prepareVariables({
pubKey,
role: 'referrer',
});
const [refereeVariables, refereeSkip] = prepareVariables({
pubKey,
role: 'referee',
});
const {
data: referrerData,
loading: referrerLoading,
error: referrerError,
refetch: referrerRefetch,
} = useReferralSetsQuery({
variables: referrerVariables,
skip: referrerSkip,
fetchPolicy: 'cache-and-network',
});
const {
data: refereeData,
loading: refereeLoading,
error: refereeError,
refetch: refereeRefetch,
} = useReferralSetsQuery({
variables: refereeVariables,
skip: refereeSkip,
fetchPolicy: 'cache-and-network',
});
const set =
referrerData?.referralSets.edges[0]?.node ||
refereeData?.referralSets.edges[0]?.node;
const role: Role | undefined = set
? set?.referrer === pubKey
? 'referrer'
: 'referee'
: undefined;
const { isEligible } = useStakeAvailable(set?.referrer);
const refetch = useCallback(() => {
referrerRefetch();
refereeRefetch();
}, [refereeRefetch, referrerRefetch]);
return {
data: set,
role,
loading: referrerLoading || refereeLoading,
error: referrerError || refereeError,
refetch,
isEligible: set ? isEligible : undefined,
};
};
export const useReferralSet = (setId?: string) => {
const [variables, skip] = prepareVariables({ setId });
const { data, loading, error, refetch } = useReferralSetsQuery({
variables,
skip,
fetchPolicy: 'cache-and-network',
});
const set = data?.referralSets.edges[0]?.node;
const { isEligible } = useStakeAvailable(set?.referrer);
return {
data: set,
loading,
error,
refetch,
isEligible: set ? isEligible : undefined,
};
};
export const useIsInReferralSet = (pubKey: string | undefined) => {
const { data } = useFindReferralSet(pubKey);
return Boolean(data);
};

View File

@ -0,0 +1,117 @@
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
import { findReferee, useReferees } from './use-referees';
import BigNumber from 'bignumber.js';
import { type BenefitTier, useReferralProgram } from './use-referral-program';
import { type StatValue } from '../constants';
import minBy from 'lodash/minBy';
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
export type RefereeStats = {
/** the discount factor -> `discountFactor` ~ `referralDiscountFactor` */
discountFactor: StatValue<BigNumber>;
/** the benefit tier matching the referee's discount factor */
benefitTier: StatValue<BenefitTier | undefined>;
/** the next benefit tier after the current referee's tier */
nextBenefitTier: StatValue<BenefitTier | undefined>;
/** the running volume */
runningVolume: StatValue<BigNumber>;
/** the number of epochs in set */
epochs: StatValue<BigNumber>;
};
const ZERO = BigNumber(0);
export const useRefereeStats = (
pubKey: string,
setId: string,
aggregationEpochs: number
): RefereeStats => {
const { data, loading, error } = useReferralSetStatsQuery({
variables: {
code: setId,
},
skip: !setId || setId.length === 0 || !pubKey || pubKey.length === 0,
fetchPolicy: 'cache-and-network',
});
const {
benefitTiers,
loading: programLoading,
error: programError,
} = useReferralProgram();
const {
data: epochData,
loading: epochsLoading,
error: epochsError,
} = useEpochInfoQuery({
fetchPolicy: 'network-only',
});
const {
data: refereesData,
loading: refereesLoading,
error: refereesError,
} = useReferees(setId, aggregationEpochs);
const referee = findReferee(pubKey, refereesData);
const stats = removePaginationWrapper(data?.referralSetStats.edges).find(
(s) => s.partyId === pubKey
);
const discountFactor = {
value: stats?.discountFactor ? BigNumber(stats.discountFactor) : ZERO,
loading: loading || refereesLoading,
error: error || refereesError,
};
const benefitTier = {
value: benefitTiers.find(
(t) =>
!discountFactor.value.isNaN() &&
!isNaN(t.discountFactor) &&
t.discountFactor === discountFactor.value.toNumber()
),
loading: programLoading || discountFactor.loading,
error: programError || discountFactor.error,
};
const nextTier = benefitTier.value?.tier
? benefitTier.value.tier + 1
: undefined;
const nextBenefitTier = {
value: nextTier
? benefitTiers.find((t) => t.tier === nextTier)
: minBy(benefitTiers, (t) => t.tier), // min tier number is lowest tier
loading: benefitTier.loading,
error: benefitTier.error,
};
const runningVolume = {
value: stats?.referralSetRunningNotionalTakerVolume
? BigNumber(stats.referralSetRunningNotionalTakerVolume)
: ZERO,
loading,
error,
};
const joinedAtEpoch = BigNumber(referee?.atEpoch || '');
const currentEpoch = BigNumber(epochData?.epoch.id || '');
const epochs = {
value:
!currentEpoch.isNaN() && !joinedAtEpoch.isNaN()
? currentEpoch.minus(joinedAtEpoch)
: ZERO,
loading: refereesLoading || epochsLoading,
error: refereesError || epochsError,
};
return {
discountFactor,
benefitTier,
nextBenefitTier,
runningVolume,
epochs,
};
};

View File

@ -0,0 +1,107 @@
import { type RefereesQuery } from './__generated__/Referees';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useRefereesQuery } from './__generated__/Referees';
import { useCallback } from 'react';
import pick from 'lodash/pick';
export type Referee = Omit<
NonNullable<RefereesQuery['referralSetReferees']['edges'][0]>['node'],
'__typename'
>;
/** The properties that can be overwritten by `propertiesOptions`. */
type RefereeProperty = keyof Pick<
Referee,
'totalRefereeGeneratedRewards' | 'totalRefereeNotionalTakerVolume'
>;
/**
* Options determining which properties should be overwritten based
* on the different `aggregationEpochs`.
*/
export type PropertiesWithDifferentAggregationEpochs = {
properties: RefereeProperty[];
aggregationEpochs: number;
};
/** Find referee by its public key (id) */
export const findReferee = (pubKey: string, referees: Referee[]) =>
referees.find((r) => r.refereeId === pubKey);
export const useReferees = (
id: string | undefined | null,
aggregationEpochs: number,
propertiesOptions?: PropertiesWithDifferentAggregationEpochs
) => {
const {
data: refereesData,
loading: refereesLoading,
error: refereesError,
refetch: refereesRefetch,
} = useRefereesQuery({
variables: {
code: id as string,
aggregationEpochs,
},
skip: !id,
fetchPolicy: 'cache-and-network',
context: { isEnlargedTimeout: true },
});
const {
data: extraData,
loading: extraLoading,
error: extraError,
refetch: extraRefetch,
} = useRefereesQuery({
variables: {
code: id as string,
aggregationEpochs: propertiesOptions?.aggregationEpochs,
},
skip:
// skip if the aggregation epochs are the same
!id ||
!propertiesOptions?.aggregationEpochs ||
propertiesOptions.aggregationEpochs === aggregationEpochs,
fetchPolicy: 'cache-and-network',
context: { isEnlargedTimeout: true },
});
let referees = [];
const refereesList = removePaginationWrapper(
refereesData?.referralSetReferees.edges
);
const extraRefereesList = removePaginationWrapper(
extraData?.referralSetReferees.edges
);
referees = refereesList.map((r) =>
overwriteProperties(r, extraRefereesList, propertiesOptions?.properties)
);
const loading = refereesLoading || extraLoading;
const error = refereesError || extraError;
const refetch = useCallback(() => {
refereesRefetch();
extraRefetch();
}, [refereesRefetch, extraRefetch]);
return { data: referees, loading, error, refetch };
};
const overwriteProperties = (
referee: Referee,
referees: Referee[],
properties?: PropertiesWithDifferentAggregationEpochs['properties']
) => {
let updatedProperties = {};
const extraRefereeData = findReferee(referee.refereeId, referees);
if (properties && extraRefereeData) {
updatedProperties = pick(extraRefereeData, properties);
}
return {
...referee,
...updatedProperties,
};
};

View File

@ -1,8 +1,12 @@
import { formatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
import {
type ReferralProgramQuery,
useReferralProgramQuery,
} from './__generated__/CurrentReferralProgram';
import BigNumber from 'bignumber.js';
import { type ApolloError } from '@apollo/client';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MOCK = {
@ -82,7 +86,37 @@ const MOCK = {
},
};
export const useReferralProgram = () => {
type ProgramDetail = Omit<
NonNullable<ReferralProgramQuery['currentReferralProgram']>,
'benefitTiers' | 'stakingTiers'
>;
export type BenefitTier = {
tier: number;
rewardFactor: number;
commission: string;
discountFactor: number;
discount: string;
minimumVolume: number;
volume: string;
epochs: number;
};
type StakingTier = {
tier: number;
minimumStakedTokens: string;
referralRewardMultiplier: string;
};
export type ReferralProgramData = {
benefitTiers: BenefitTier[];
stakingTiers: StakingTier[];
details: ProgramDetail | undefined;
loading: boolean;
error?: ApolloError;
};
export const useReferralProgram = (): ReferralProgramData => {
const { data, loading, error } = useReferralProgramQuery({
fetchPolicy: 'cache-and-network',
});

View File

@ -5,20 +5,22 @@ import {
ToastHeading,
Button,
} from '@vegaprotocol/ui-toolkit';
import { useReferral } from './use-referral';
import { useVegaWallet } from '@vegaprotocol/wallet-react';
import { useEffect } from 'react';
import { useT } from '../../../lib/use-t';
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { Routes } from '../../../lib/links';
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
import { useFindReferralSet } from './use-find-referral-set';
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
const useNonEligibleReferralSet = () => {
const { pubKey } = useVegaWallet();
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
const { data, loading, role, isEligible, refetch } =
useFindReferralSet(pubKey);
const {
data: epochData,
loading: epochLoading,
@ -36,7 +38,13 @@ const useNonEligibleReferralSet = () => {
};
}, [epochRefetch, refetch]);
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
return {
data,
isEligible,
role,
epoch: epochData?.epoch.id,
loading: loading || epochLoading,
};
};
export const useReferralToasts = () => {
@ -49,14 +57,16 @@ export const useReferralToasts = () => {
store.update,
]);
const { data, epoch, loading } = useNonEligibleReferralSet();
const { data, role, isEligible, epoch, loading } =
useNonEligibleReferralSet();
useEffect(() => {
if (
data &&
role === 'referee' &&
epoch &&
!loading &&
!data.isEligible &&
!isEligible &&
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
) {
const nonEligibleReferralToast: Toast = {
@ -98,9 +108,11 @@ export const useReferralToasts = () => {
data,
epoch,
hasToast,
isEligible,
loading,
navigate,
pathname,
role,
setToast,
t,
updateToast,

View File

@ -1,217 +0,0 @@
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useCallback } from 'react';
import { useRefereesQuery } from './__generated__/Referees';
import compact from 'lodash/compact';
import pick from 'lodash/pick';
import type {
ReferralSetsQuery,
ReferralSetsQueryVariables,
} from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30;
export type Role = 'referrer' | 'referee';
type UseReferralArgs = (
| { code: string | undefined }
| { pubKey: string | undefined; role: Role }
) & {
aggregationEpochs?: number;
};
const prepareVariables = (
args: UseReferralArgs
): [ReferralSetsQueryVariables, boolean] => {
const byCode = 'code' in args;
const byRole = 'pubKey' in args && 'role' in args;
let variables = {};
let skip = true;
if (byCode) {
variables = {
id: args.code,
};
skip = !args.code;
}
if (byRole) {
if (args.role === 'referee') {
variables = { referee: args.pubKey };
}
if (args.role === 'referrer') {
variables = { referrer: args.pubKey };
}
skip = !args.pubKey;
}
return [variables, skip];
};
export const useReferral = (args: UseReferralArgs) => {
const [variables, skip] = prepareVariables(args);
const {
data: referralData,
loading: referralLoading,
error: referralError,
refetch: referralRefetch,
} = useReferralSetsQuery({
variables,
skip,
fetchPolicy: 'cache-and-network',
});
// A user can only have 1 active referral program at a time
const referralSet =
referralData?.referralSets.edges &&
referralData.referralSets.edges.length > 0
? referralData.referralSets.edges[0]?.node
: undefined;
const { isEligible } = useStakeAvailable(referralSet?.referrer);
const {
data: refereesData,
loading: refereesLoading,
error: refereesError,
refetch: refereesRefetch,
} = useRefereesQuery({
variables: {
code: referralSet?.id as string,
aggregationEpochs:
args.aggregationEpochs !== null
? args.aggregationEpochs
: DEFAULT_AGGREGATION_DAYS,
},
skip: !referralSet?.id,
fetchPolicy: 'cache-and-network',
context: { isEnlargedTimeout: true },
});
const referees = compact(
removePaginationWrapper(refereesData?.referralSetReferees.edges)
);
const refetch = useCallback(() => {
referralRefetch();
refereesRefetch();
}, [refereesRefetch, referralRefetch]);
const byReferee =
'role' in args && 'pubKey' in args && args.role === 'referee';
const referee = byReferee
? referees.find((r) => r.refereeId === args.pubKey) || null
: null;
const data =
referralSet && refereesData
? {
code: referralSet.id,
role: 'role' in args ? args.role : null,
referee: referee,
referrerId: referralSet.referrer,
createdAt: referralSet.createdAt,
isEligible,
referees,
}
: undefined;
return {
data,
loading: referralLoading || refereesLoading,
error: referralError || refereesError,
refetch,
};
};
type Referee = NonNullable<
NonNullable<ReturnType<typeof useReferral>['data']>['referee']
>;
type RefereeProperties = (keyof Referee)[];
const findReferee = (referee: Referee, referees: Referee[]) =>
referees.find((r) => r.refereeId === referee?.refereeId) || referee;
const updateReferee = (
referee: Referee,
referees: Referee[],
properties: RefereeProperties
) => ({
...referee,
...pick(findReferee(referee, referees), properties),
});
export const useUpdateReferees = (
referral: ReturnType<typeof useReferral>,
aggregationEpochs: number,
properties: RefereeProperties,
skip?: boolean
): ReturnType<typeof useReferral> => {
const { data, loading, error, refetch } = useRefereesQuery({
variables: {
code: referral?.data?.code as string,
aggregationEpochs,
},
skip: skip || !referral?.data?.code,
fetchPolicy: 'cache-and-network',
context: { isEnlargedTimeout: true },
});
const refetchAll = useCallback(() => {
refetch();
referral.refetch();
}, [refetch, referral]);
if (!referral.data || skip) {
return referral;
}
const referees = compact(
removePaginationWrapper(data?.referralSetReferees.edges)
);
return {
data: data && {
...referral.data,
referees: referral.data.referees.map((referee) =>
updateReferee(referee, referees, properties)
),
referee:
referral.data.referee &&
updateReferee(referral.data.referee, referees, properties),
},
loading: loading || referral.loading,
error: error || referral.error,
refetch: refetchAll,
};
};
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
data?.referralSets.edges && data.referralSets.edges.length > 0
? data.referralSets.edges[0]?.node
: undefined;
export const useIsInReferralSet = (pubKey: string | undefined) => {
const [asRefereeVariables, asRefereeSkip] = prepareVariables({
pubKey,
role: 'referee',
});
const [asReferrerVariables, asReferrerSkip] = prepareVariables({
pubKey,
role: 'referrer',
});
const { data: asRefereeData } = useReferralSetsQuery({
variables: asRefereeVariables,
skip: asRefereeSkip,
fetchPolicy: 'cache-and-network',
});
const { data: asReferrerData } = useReferralSetsQuery({
variables: asReferrerVariables,
skip: asReferrerSkip,
fetchPolicy: 'cache-and-network',
});
return Boolean(
retrieveReferralSetData(asRefereeData) ||
retrieveReferralSetData(asReferrerData)
);
};

View File

@ -0,0 +1,104 @@
import { useReferralSetStatsQuery } from './__generated__/ReferralSetStats';
import BigNumber from 'bignumber.js';
import { useReferees } from './use-referees';
import { type StatValue } from '../constants';
export type ReferrerStats = {
/** the base commission -> `rewardFactor` ~ `referralRewardFactor` */
baseCommission: StatValue<BigNumber>;
/** the staking multiplier -> `rewardsMultiplier` ~ `referralRewardMultiplier` */
multiplier: StatValue<BigNumber>;
/** the final commission -> base * multiplier */
finalCommission: StatValue<BigNumber>;
/** the referrer taker volume -> `referrerTakerVolume` */
volume: StatValue<BigNumber>;
/** the number of referees -> referees query required */
referees: StatValue<BigNumber>;
/** the total commission -> sum of `totalRefereeGeneratedRewards` */
totalCommission: StatValue<BigNumber>;
runningVolume: StatValue<BigNumber>;
};
const ZERO = BigNumber(0);
const ONE = BigNumber(1);
export const useReferrerStats = (
setId: string,
aggregationEpochs: number
): ReferrerStats => {
const { data, loading, error } = useReferralSetStatsQuery({
variables: {
code: setId,
},
skip: !setId || setId.length === 0,
fetchPolicy: 'cache-and-network',
});
const {
data: refereesData,
loading: refereesLoading,
error: refereesError,
} = useReferees(setId, aggregationEpochs);
const statsAvailable = data?.referralSetStats.edges[0]?.node;
const baseCommission = {
value: statsAvailable ? BigNumber(statsAvailable.rewardFactor) : ZERO,
loading,
error,
};
const multiplier = {
value: statsAvailable ? BigNumber(statsAvailable.rewardsMultiplier) : ONE,
loading,
error,
};
const finalCommission = {
value: !multiplier.value.isNaN()
? baseCommission.value
: new BigNumber(multiplier.value).times(baseCommission.value),
loading,
error,
};
const volume = {
value: statsAvailable
? BigNumber(statsAvailable.referrerTakerVolume)
: ZERO,
loading,
error,
};
const referees = {
value: BigNumber(refereesData.length),
loading: refereesLoading,
error: refereesError,
};
const totalCommission = {
value: refereesData
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
.reduce((all, r) => all.plus(r), ZERO),
loading: refereesLoading,
error: refereesError,
};
const runningVolume = {
value: statsAvailable?.referralSetRunningNotionalTakerVolume
? BigNumber(statsAvailable.referralSetRunningNotionalTakerVolume)
: ZERO,
loading,
error,
};
return {
baseCommission,
multiplier,
finalCommission,
volume,
referees,
totalCommission,
runningVolume,
};
};

View File

@ -0,0 +1,202 @@
import classNames from 'classnames';
import { useRefereeStats } from './hooks/use-referee-stats';
import {
BenefitTierTile,
DiscountTile,
EpochsTile,
NextTierEpochsTile,
NextTierVolumeTile,
RunningVolumeTile,
TeamTile,
} from './tiles';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import { CodeTile } from './tile';
import { useT } from '../../lib/use-t';
import { ApplyCodeForm } from './apply-code-form';
import { useVegaWallet } from '@vegaprotocol/wallet-react';
import { useReferralProgram } from './hooks/use-referral-program';
import { DEFAULT_AGGREGATION_DAYS } from './constants';
import { useReferralSet } from './hooks/use-find-referral-set';
import { Loader } from '@vegaprotocol/ui-toolkit';
import minBy from 'lodash/minBy';
import BigNumber from 'bignumber.js';
export const RefereeStatistics = ({
aggregationEpochs,
setId,
pubKey,
referrerPubKey,
}: {
/** The aggregation epochs used to calculate statistics. */
aggregationEpochs: number;
/** The set id (code). */
setId: string;
/** The referee public key. */
pubKey: string;
/** The referrer's public key. */
referrerPubKey: string;
}) => {
const t = useT();
const {
benefitTier,
discountFactor,
epochs,
nextBenefitTier,
runningVolume,
} = useRefereeStats(pubKey, setId, aggregationEpochs);
const { isEligible } = useStakeAvailable(referrerPubKey);
return (
<>
<div
data-testid="referral-statistics"
data-as="referee"
className="relative mx-auto mb-20"
>
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
{/** TEAM TILE - referral set id is the same as team id */}
<TeamTile teamId={setId} />
{/** TILES ROW 1 */}
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
<BenefitTierTile
benefitTier={benefitTier}
nextBenefitTier={nextBenefitTier}
/>
<RunningVolumeTile
aggregationEpochs={aggregationEpochs}
runningVolume={runningVolume}
/>
<CodeTile code={setId} />
</div>
{/** TILES ROW 2 */}
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<DiscountTile discountFactor={discountFactor} />
<NextTierVolumeTile
nextBenefitTier={nextBenefitTier}
runningVolume={runningVolume}
/>
<EpochsTile epochs={epochs} />
<NextTierEpochsTile
epochs={epochs}
nextBenefitTier={nextBenefitTier}
/>
</div>
</div>
{/** ELIGIBILITY WARNING */}
{!isEligible ? (
<div
data-testid="referral-eligibility-warning"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
>
<h2 className="text-2xl mb-2">
{t('Referral code no longer valid')}
</h2>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
)}
</p>
</div>
) : null}
</div>
{!isEligible && <ApplyCodeForm />}
</>
);
};
export const PreviewRefereeStatistics = ({ setId }: { setId: string }) => {
const program = useReferralProgram();
const aggregationEpochs =
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
const { pubKey } = useVegaWallet();
const { data: referralSet, loading } = useReferralSet(setId);
const { epochs, runningVolume } = useRefereeStats(
pubKey || '',
referralSet?.id || '',
aggregationEpochs
);
if (loading) {
return (
<div
data-testid="referral-statistics"
data-as="referee"
data-preview
className="relative mx-auto mb-20"
>
<Loader size="small" />
</div>
);
}
if (!referralSet) {
return null;
}
const stat = <T,>(value: T) => ({
value,
loading: false,
error: undefined,
});
const firstBenefitTier = stat(minBy(program.benefitTiers, (bt) => bt.epochs));
const nextBenefitTier = stat(
program.benefitTiers.find(
(bt) =>
bt.tier ===
(firstBenefitTier.value?.tier
? firstBenefitTier.value.tier + 1
: undefined)
)
);
const discountFactor = stat(
firstBenefitTier.value?.discountFactor
? BigNumber(firstBenefitTier.value?.discountFactor)
: BigNumber(0)
);
return (
<div
data-testid="referral-statistics"
data-as="referee"
data-preview
className="relative mx-auto mb-20"
>
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
{/** TEAM TILE - referral set id is the same as team id */}
<TeamTile teamId={setId} />
{/** TILES ROW 1 */}
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
<BenefitTierTile
benefitTier={firstBenefitTier}
nextBenefitTier={nextBenefitTier}
/>
<RunningVolumeTile
aggregationEpochs={aggregationEpochs}
runningVolume={runningVolume}
/>
<CodeTile code={setId} />
</div>
{/** TILES ROW 2 */}
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<DiscountTile discountFactor={discountFactor} />
<NextTierVolumeTile
nextBenefitTier={nextBenefitTier}
runningVolume={runningVolume}
/>
<EpochsTile epochs={epochs} />
<NextTierEpochsTile
epochs={epochs}
nextBenefitTier={nextBenefitTier}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,146 @@
import { useLayoutEffect, useRef, useState } from 'react';
import { ns, useT } from '../../lib/use-t';
import classNames from 'classnames';
import {
Loader,
Tooltip,
VegaIcon,
VegaIconNames,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import { Table } from '../../components/table';
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import { Trans } from 'react-i18next';
import { QUSDTooltip } from './qusd-tooltip';
import { type Referee, useReferees } from './hooks/use-referees';
import { DEFAULT_AGGREGATION_DAYS } from './constants';
export const Referees = ({
setId,
aggregationEpochs,
}: {
setId: string;
aggregationEpochs: number;
}) => {
const { data, loading } = useReferees(setId, aggregationEpochs, {
// get total referree generated rewards for the last 30 days
aggregationEpochs: DEFAULT_AGGREGATION_DAYS,
properties: ['totalRefereeGeneratedRewards'],
});
if (loading) {
return <Loader size="small" />;
}
return <RefereesTable data={data} aggregationEpochs={aggregationEpochs} />;
};
export const RefereesTable = ({
data: referees,
aggregationEpochs,
}: {
data: Referee[];
aggregationEpochs: number;
}) => {
const t = useT();
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
return (
<>
{/* Referees (only for referrer view) */}
{referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
<div
className={classNames(
collapsed && [
'relative max-h-96 overflow-hidden',
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
]
)}
>
<button
className={classNames(
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
{
hidden: !collapsed,
}
)}
onClick={() => setCollapsed(false)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
</button>
<Table
ref={tableRef}
columns={[
{ name: 'party', displayName: t('Trader') },
{ name: 'joined', displayName: t('Date Joined') },
{
name: 'volume',
displayName: t(
'volumeLastEpochs',
'Volume (last {{count}} epochs)',
{
count: aggregationEpochs,
}
),
},
{
// NOTE: This should be gotten for the last 30 days regardless of the program's window length
name: 'commission',
displayName: (
<Trans
i18nKey="referralStatisticsCommission"
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
components={[
<QUSDTooltip key="0" />,
<Tooltip
key="1"
description={t(
'Depending on data node retention you may not be able see the full 30 days'
)}
>
<span>last 30 epochs</span>
</Tooltip>,
]}
values={{
count: DEFAULT_AGGREGATION_DAYS,
}}
ns={ns}
/>
),
},
]}
data={sortBy(
referees.map((r) => ({
party: (
<span title={r.refereeId}>
{truncateMiddle(r.refereeId)}
</span>
),
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
volume: Number(r.totalRefereeNotionalTakerVolume),
commission: Number(r.totalRefereeGeneratedRewards),
})),
(r) => r.volume
)
.map((r) => ({
...r,
volume: formatNumber(r.volume, 0),
commission: formatNumber(r.commission, 0),
}))
.reverse()}
/>
</div>
</div>
)}
</>
);
};

View File

@ -1,597 +1,58 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import BigNumber from 'bignumber.js';
import minBy from 'lodash/minBy';
import sortBy from 'lodash/sortBy';
import compact from 'lodash/compact';
import { Trans } from 'react-i18next';
import classNames from 'classnames';
import {
VegaIcon,
VegaIconNames,
truncateMiddle,
TextChildrenTooltip as Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { Loader } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet-react';
import {
addDecimalsFormatNumber,
formatNumber,
getDateFormat,
getDateTimeFormat,
getUserLocale,
removePaginationWrapper,
} from '@vegaprotocol/utils';
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import { useT, ns } from '../../lib/use-t';
import { useTeam } from '../../lib/hooks/use-team';
import { TeamAvatar } from '../../components/competitions/team-avatar';
import { TeamStats } from '../../components/competitions/team-stats';
import { Table } from '../../components/table';
import {
DEFAULT_AGGREGATION_DAYS,
useReferral,
useUpdateReferees,
} from './hooks/use-referral';
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
import { ApplyCodeFormContainer } from './apply-code-form';
import { useReferralProgram } from './hooks/use-referral-program';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { QUSDTooltip } from './qusd-tooltip';
import { CodeTile, StatTile, Tile } from './tile';
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
import { useFindReferralSet } from './hooks/use-find-referral-set';
import { Referees } from './referees';
import { ReferrerStatistics } from './referrer-statistics';
import { RefereeStatistics } from './referee-statistics';
import { DEFAULT_AGGREGATION_DAYS } from './constants';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
const program = useReferralProgram();
const { data: referee, refetch: refereeRefetch } = useReferral({
pubKey,
role: 'referee',
aggregationEpochs: program.details?.windowLength,
});
const {
data: referralSet,
loading: referralSetLoading,
role,
refetch,
} = useFindReferralSet(pubKey);
const { data: referrer, refetch: referrerRefetch } = useUpdateReferees(
useReferral({
pubKey,
role: 'referrer',
aggregationEpochs: program.details?.windowLength,
}),
DEFAULT_AGGREGATION_DAYS,
['totalRefereeGeneratedRewards'],
DEFAULT_AGGREGATION_DAYS === program.details?.windowLength
);
if (referralSetLoading) {
return <Loader size="small" />;
}
const refetch = useCallback(() => {
refereeRefetch();
referrerRefetch();
}, [refereeRefetch, referrerRefetch]);
const aggregationEpochs =
program.details?.windowLength || DEFAULT_AGGREGATION_DAYS;
if (referee?.code) {
if (referralSet?.id && role === 'referrer') {
return (
<>
<Statistics data={referee} program={program} as="referee" />
{!referee.isEligible && <ApplyCodeForm />}
<ReferrerStatistics
aggregationEpochs={aggregationEpochs}
createdAt={referralSet.createdAt}
setId={referralSet.id}
/>
<Referees
setId={referralSet.id}
aggregationEpochs={aggregationEpochs}
/>
</>
);
}
if (referrer?.code) {
if (pubKey && referralSet?.id && role === 'referee') {
return (
<>
<Statistics data={referrer} program={program} as="referrer" />
<RefereesTable data={referrer} program={program} />
</>
<RefereeStatistics
aggregationEpochs={aggregationEpochs}
pubKey={pubKey}
referrerPubKey={referralSet.referrer}
setId={referralSet.id}
/>
);
}
return <ApplyCodeFormContainer onSuccess={refetch} />;
};
export const useStats = ({
data,
program,
}: {
data?: NonNullable<ReturnType<typeof useReferral>['data']>;
program: ReturnType<typeof useReferralProgram>;
}) => {
const { benefitTiers } = program;
const { data: epochData } = useEpochInfoQuery({
fetchPolicy: 'network-only',
});
const { data: statsData } = useReferralSetStatsQuery({
variables: {
code: data?.code || '',
},
skip: !data?.code,
fetchPolicy: 'cache-and-network',
});
const currentEpoch = Number(epochData?.epoch.id);
const stats =
statsData?.referralSetStats.edges &&
compact(removePaginationWrapper(statsData.referralSetStats.edges));
const refereeInfo = data?.referee;
const refereeStats = stats?.find(
(r) => r.partyId === data?.referee?.refereeId
);
const statsAvailable = stats && stats.length > 0 && stats[0];
const baseCommissionValue = statsAvailable
? Number(statsAvailable.rewardFactor)
: 0;
const runningVolumeValue = statsAvailable
? Number(statsAvailable.referralSetRunningNotionalTakerVolume)
: 0;
const referrerVolumeValue = statsAvailable
? Number(statsAvailable.referrerTakerVolume)
: 0;
const multiplier = statsAvailable
? Number(statsAvailable.rewardsMultiplier)
: 1;
const finalCommissionValue = isNaN(multiplier)
? baseCommissionValue
: new BigNumber(multiplier).times(baseCommissionValue).toNumber();
const discountFactorValue = refereeStats?.discountFactor
? Number(refereeStats.discountFactor)
: 0;
const currentBenefitTierValue = benefitTiers.find(
(t) =>
!isNaN(discountFactorValue) &&
!isNaN(t.discountFactor) &&
t.discountFactor === discountFactorValue
);
const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch
? currentEpoch - refereeInfo?.atEpoch
: 0;
const nextBenefitTierVolumeValue = nextBenefitTierValue
? nextBenefitTierValue.minimumVolume - runningVolumeValue
: 0;
const nextBenefitTierEpochsValue = nextBenefitTierValue
? nextBenefitTierValue.epochs - epochsValue
: 0;
return {
baseCommissionValue,
runningVolumeValue,
referrerVolumeValue,
multiplier,
finalCommissionValue,
discountFactorValue,
currentBenefitTierValue,
nextBenefitTierValue,
epochsValue,
nextBenefitTierVolumeValue,
nextBenefitTierEpochsValue,
};
};
export const Statistics = ({
data,
program,
as,
}: {
data: NonNullable<ReturnType<typeof useReferral>['data']>;
program: ReturnType<typeof useReferralProgram>;
as: 'referrer' | 'referee';
}) => {
const t = useT();
const {
baseCommissionValue,
runningVolumeValue,
referrerVolumeValue,
multiplier,
finalCommissionValue,
discountFactorValue,
currentBenefitTierValue,
epochsValue,
nextBenefitTierValue,
nextBenefitTierVolumeValue,
nextBenefitTierEpochsValue,
} = useStats({ data, program });
const isApplyCodePreview = data.referee === null;
const { benefitTiers } = useReferralProgram();
const { stakeAvailable, isEligible } = useStakeAvailable();
const { details } = program;
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
notation: 'compact',
compactDisplay: 'short',
});
const baseCommissionTile = (
<StatTile
title={t('Base commission rate')}
description={t(
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
{
runningVolume: compactNumFormat.format(runningVolumeValue),
epochs: (
details?.windowLength || DEFAULT_AGGREGATION_DAYS
).toString(),
}
)}
testId="base-commission-rate"
overrideWithNoProgram={!details}
>
{baseCommissionValue * 100}%
</StatTile>
);
const stakingMultiplierTile = (
<StatTile
title={t('Staking multiplier')}
testId="staking-multiplier"
description={
<span
className={classNames({
'text-vega-red': !isEligible,
})}
>
{t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(
stakeAvailable?.toString() || 0,
18
),
})}
</span>
}
overrideWithNoProgram={!details}
>
{multiplier || t('None')}
</StatTile>
);
const baseCommissionFormatted = BigNumber(baseCommissionValue)
.times(100)
.toString();
const finalCommissionFormatted = new BigNumber(finalCommissionValue)
.times(100)
.toString();
const finalCommissionTile = (
<StatTile
title={t('Final commission rate')}
description={
!isNaN(multiplier)
? `(${baseCommissionFormatted}% ⨉ ${multiplier} = ${finalCommissionFormatted}%)`
: undefined
}
testId="final-commission-rate"
overrideWithNoProgram={!details}
>
{finalCommissionFormatted}%
</StatTile>
);
const numberOfTradersValue = data.referees.length;
const numberOfTradersTile = (
<StatTile title={t('Number of traders')} testId="number-of-traders">
{numberOfTradersValue}
</StatTile>
);
const codeTile = (
<CodeTile
code={data?.code}
createdAt={getDateFormat().format(new Date(data.createdAt))}
/>
);
const referrerVolumeTile = (
<StatTile
title={t('myVolume', 'My volume (last {{count}} epochs)', {
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
})}
testId="my-volume"
overrideWithNoProgram={!details}
>
{compactNumFormat.format(referrerVolumeValue)}
</StatTile>
);
const totalCommissionValue = data.referees
.map((r) => new BigNumber(r.totalRefereeGeneratedRewards))
.reduce((all, r) => all.plus(r), new BigNumber(0));
const totalCommissionTile = (
<StatTile
testId="total-commission"
title={
<Trans
i18nKey="totalCommission"
defaults="Total commission (<0>last {{count}} epochs</0>)"
values={{
count: DEFAULT_AGGREGATION_DAYS,
}}
components={[
<Tooltip
key="1"
description={t(
'Depending on data node retention you may not be able see the full 30 days'
)}
>
last 30 epochs
</Tooltip>,
]}
/>
}
description={<QUSDTooltip />}
>
{formatNumber(totalCommissionValue, 0)}
</StatTile>
);
const currentBenefitTierTile = (
<StatTile
title={t('Current tier')}
testId="current-tier"
description={
nextBenefitTierValue?.tier
? t('(Next tier: {{nextTier}})', {
nextTier: nextBenefitTierValue?.tier,
})
: undefined
}
overrideWithNoProgram={!details}
>
{isApplyCodePreview
? currentBenefitTierValue?.tier || benefitTiers[0]?.tier || 'None'
: currentBenefitTierValue?.tier || 'None'}
</StatTile>
);
const discountFactorTile = (
<StatTile
title={t('Discount')}
testId="discount"
overrideWithNoProgram={!details}
>
{isApplyCodePreview && benefitTiers.length >= 1
? benefitTiers[0].discountFactor * 100
: discountFactorValue * 100}
%
</StatTile>
);
const runningVolumeTile = (
<StatTile
title={t(
'runningNotionalOverEpochs',
'Combined volume (last {{count}} epochs)',
{
count: details?.windowLength,
}
)}
testId="combined-volume"
overrideWithNoProgram={!details}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
);
const epochsTile = (
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
{epochsValue}
</StatTile>
);
const nextTierVolumeTile = (
<StatTile
title={t('Volume to next tier')}
testId="vol-to-next-tier"
overrideWithNoProgram={!details}
>
{nextBenefitTierVolumeValue <= 0
? '0'
: compactNumFormat.format(nextBenefitTierVolumeValue)}
</StatTile>
);
const nextTierEpochsTile = (
<StatTile
title={t('Epochs to next tier')}
testId="epochs-to-next-tier"
overrideWithNoProgram={!details}
>
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
</StatTile>
);
const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
<div
data-testid="referral-eligibility-warning"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
>
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
)}
</p>
</div>
);
const referrerTiles = (
<>
<Team teamId={data.code} />
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
{baseCommissionTile}
{stakingMultiplierTile}
{finalCommissionTile}
</div>
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
{codeTile}
{referrerVolumeTile}
{numberOfTradersTile}
{totalCommissionTile}
</div>
</>
);
const refereeTiles = (
<>
<Team teamId={data.code} />
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
{currentBenefitTierTile}
{runningVolumeTile}
{codeTile}
</div>
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
{discountFactorTile}
{nextTierVolumeTile}
{epochsTile}
{nextTierEpochsTile}
</div>
</>
);
return (
<div
data-testid="referral-statistics"
data-as={as}
className="relative mx-auto mb-20"
>
<div
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
})}
>
{as === 'referrer' && referrerTiles}
{as === 'referee' && refereeTiles}
</div>
{eligibilityWarningOverlay}
</div>
);
};
export const RefereesTable = ({
data,
program,
}: {
data: NonNullable<ReturnType<typeof useReferral>['data']>;
program: ReturnType<typeof useReferralProgram>;
}) => {
const t = useT();
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
const { details } = program;
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
return (
<>
{/* Referees (only for referrer view) */}
{data.referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
<div
className={classNames(
collapsed && [
'relative max-h-96 overflow-hidden',
'after:w-full after:h-20 after:absolute after:bottom-0 after:left-0',
'after:bg-gradient-to-t after:from-white after:dark:from-vega-cdark-900 after:to-transparent',
]
)}
>
<button
className={classNames(
'absolute left-1/2 bottom-0 z-10 p-2 translate-x-[-50%]',
{
hidden: !collapsed,
}
)}
onClick={() => setCollapsed(false)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={24} />
</button>
<Table
ref={tableRef}
columns={[
{ name: 'party', displayName: t('Trader') },
{ name: 'joined', displayName: t('Date Joined') },
{
name: 'volume',
displayName: t(
'volumeLastEpochs',
'Volume (last {{count}} epochs)',
{
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
}
),
},
{
name: 'commission',
displayName: (
<Trans
i18nKey="referralStatisticsCommission"
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
components={[
<QUSDTooltip key="0" />,
<Tooltip
key="1"
description={t(
'Depending on data node retention you may not be able see the full 30 days'
)}
>
last 30 epochs
</Tooltip>,
]}
values={{
count: DEFAULT_AGGREGATION_DAYS,
}}
ns={ns}
/>
),
},
]}
data={sortBy(
data.referees.map((r) => ({
party: (
<span title={r.refereeId}>
{truncateMiddle(r.refereeId)}
</span>
),
joined: getDateTimeFormat().format(new Date(r.joinedAt)),
volume: Number(r.totalRefereeNotionalTakerVolume),
commission: Number(r.totalRefereeGeneratedRewards),
})),
(r) => r.volume
)
.map((r) => ({
...r,
volume: formatNumber(r.volume, 0),
commission: formatNumber(r.commission, 0),
}))
.reverse()}
/>
</div>
</div>
)}
</>
);
};
const Team = ({ teamId }: { teamId?: string }) => {
const { team, members } = useTeam(teamId);
const { data: games } = useGames(teamId);
if (!team) return null;
return (
<Tile className="flex gap-3 lg:gap-4">
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
<div className="flex flex-col items-start gap-1 lg:gap-3">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
<TeamStats
members={members}
games={areTeamGames(games) ? games : undefined}
/>
</div>
</Tile>
);
};

View File

@ -10,12 +10,12 @@ import { TabLink } from './buttons';
import { Outlet, useMatch } from 'react-router-dom';
import { Routes } from '../../lib/links';
import { useVegaWallet } from '@vegaprotocol/wallet-react';
import { useReferral } from './hooks/use-referral';
import { REFERRAL_DOCS_LINK } from './constants';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { useFindReferralSet } from './hooks/use-find-referral-set';
const Nav = () => {
const t = useT();
@ -34,26 +34,9 @@ export const Referrals = () => {
const t = useT();
const { pubKey } = useVegaWallet();
const {
data: referee,
loading: refereeLoading,
error: refereeError,
} = useReferral({
pubKey,
role: 'referee',
});
const {
data: referrer,
loading: referrerLoading,
error: referrerError,
} = useReferral({
pubKey,
role: 'referrer',
});
const { data, loading, error } = useFindReferralSet(pubKey);
const error = refereeError || referrerError;
const loading = refereeLoading || referrerLoading;
const showNav = !loading && !error && !referrer && !referee;
const showNav = !loading && !error && !data;
usePageTitle(t('Referrals'));

View File

@ -0,0 +1,73 @@
import classNames from 'classnames';
import { useReferrerStats } from './hooks/use-referrer-stats';
import {
BaseCommissionTile,
FinalCommissionTile,
RefereesTile,
StakingMultiplierTile,
TeamTile,
TotalCommissionTile,
VolumeTile,
dateFormatter,
} from './tiles';
import { CodeTile } from './tile';
export const ReferrerStatistics = ({
aggregationEpochs,
setId,
createdAt,
}: {
/** The aggregation epochs used to calculate statistics. */
aggregationEpochs: number;
/** The set id (code). */
setId: string;
/** The referral set date of creation. */
createdAt: string;
}) => {
const {
baseCommission,
finalCommission,
multiplier,
referees,
runningVolume,
totalCommission,
volume,
} = useReferrerStats(setId, aggregationEpochs);
return (
<div
data-testid="referral-statistics"
data-as="referrer"
className="relative mx-auto mb-20"
>
<div className={classNames('grid grid-cols-1 grid-rows-1 gap-5')}>
{/** TEAM TILE - referral set id is the same as team id */}
<TeamTile teamId={setId} />
{/** TILES ROW 1 */}
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
<BaseCommissionTile
aggregationEpochs={aggregationEpochs}
baseCommission={baseCommission}
runningVolume={runningVolume}
/>
<StakingMultiplierTile multiplier={multiplier} />
<FinalCommissionTile
baseCommission={baseCommission}
multiplier={multiplier}
finalCommission={finalCommission}
/>
</div>
{/** TILES ROW 2 */}
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
<CodeTile code={setId} createdAt={dateFormatter(createdAt)} />
<VolumeTile aggregationEpochs={aggregationEpochs} volume={volume} />
<RefereesTile referees={referees} />
<TotalCommissionTile
aggregationEpochs={aggregationEpochs}
totalCommission={totalCommission}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,440 @@
import { addDecimalsFormatNumber, getDateFormat } from '@vegaprotocol/utils';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import { useT } from '../../lib/use-t';
import BigNumber from 'bignumber.js';
import classNames from 'classnames';
import { type ReactNode } from 'react';
import { Trans } from 'react-i18next';
import { type StatValue, COMPACT_NUMBER_FORMAT } from './constants';
import { type ReferrerStats } from './hooks/use-referrer-stats';
import { type RefereeStats } from './hooks/use-referee-stats';
import { QUSDTooltip } from './qusd-tooltip';
import { NoProgramTile, StatTile, Tile } from './tile';
import { Loader, Tooltip } from '@vegaprotocol/ui-toolkit';
import { type BenefitTier } from './hooks/use-referral-program';
import { useTeam } from '../../lib/hooks/use-team';
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
import { TeamAvatar } from '../../components/competitions/team-avatar';
import { TeamStats } from '../../components/competitions/team-stats';
/* Formatters */
const percentageFormatter = (value: BigNumber) =>
value.times(100).toFixed(2) + '%';
const compactFormatter =
(maximumFractionDigits = 2) =>
(value: BigNumber) =>
COMPACT_NUMBER_FORMAT(maximumFractionDigits).format(value.toNumber());
const valueFormatter = (noValueLabel: string) => (value: BigNumber) => {
if (value.isNaN() || value.isZero()) {
return noValueLabel;
}
return value.toString();
};
export const dateFormatter = (value: string) => {
try {
return getDateFormat().format(new Date(value));
} catch {
return '-';
}
};
/* Helpers */
const Value = <T,>({
data: { value, loading, error },
formatter,
}: {
data: StatValue<T>;
formatter: (value: T) => ReactNode;
}) => {
if (loading) {
return (
<span className="p-[33px]">
<Loader size="small" />
</span>
);
}
if (error) {
return <span data-error={error.message}>-</span>;
}
return formatter(value);
};
/* Referrer tiles */
export const BaseCommissionTile = ({
baseCommission,
runningVolume,
aggregationEpochs,
}: {
baseCommission: ReferrerStats['baseCommission'];
runningVolume: ReferrerStats['runningVolume'];
aggregationEpochs: number;
}) => {
const t = useT();
const runningVolumeDescription = compactFormatter(2)(runningVolume.value);
const description = t(
'(Combined set volume {{runningVolume}} over last {{epochs}} epochs)',
{
runningVolume: runningVolumeDescription,
epochs: aggregationEpochs.toString(),
}
);
return (
<StatTile
title={t('Base commission rate')}
description={description}
testId="base-commission-rate"
>
<Value data={baseCommission} formatter={percentageFormatter} />
</StatTile>
);
};
export const StakingMultiplierTile = ({
multiplier,
}: {
multiplier: ReferrerStats['multiplier'];
}) => {
const t = useT();
const { stakeAvailable, isEligible } = useStakeAvailable();
const description = (
<span
className={classNames({
'text-vega-red': !isEligible,
})}
>
{t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
})}
</span>
);
return (
<StatTile
title={t('Staking multiplier')}
description={description}
testId="staking-multiplier"
>
<Value data={multiplier} formatter={valueFormatter(t('None'))} />
</StatTile>
);
};
export const FinalCommissionTile = ({
baseCommission,
multiplier,
finalCommission,
}: {
baseCommission: ReferrerStats['baseCommission'];
multiplier: ReferrerStats['multiplier'];
finalCommission: ReferrerStats['finalCommission'];
}) => {
const t = useT();
const description =
!baseCommission.loading && !finalCommission.loading && !multiplier.loading
? `(${percentageFormatter(
baseCommission.value
)} &times; ${multiplier.value.toString()} = ${percentageFormatter(
finalCommission.value
)})`
: undefined;
return (
<StatTile
title={t('Final commission rate')}
description={description}
testId="final-commission-rate"
>
<Value data={finalCommission} formatter={percentageFormatter} />
</StatTile>
);
};
export const VolumeTile = ({
volume,
aggregationEpochs,
}: {
volume: ReferrerStats['volume'];
aggregationEpochs: number;
}) => {
const t = useT();
return (
<StatTile
title={t('myVolume', 'My volume (last {{count}} epochs)', {
count: aggregationEpochs,
})}
testId="my-volume"
>
<Value data={volume} formatter={compactFormatter(2)} />
</StatTile>
);
};
export const TotalCommissionTile = ({
totalCommission,
aggregationEpochs,
}: {
totalCommission: ReferrerStats['totalCommission'];
aggregationEpochs: number;
}) => {
const t = useT();
return (
<StatTile
testId="total-commission"
title={
<Trans
i18nKey="totalCommission"
defaults="Total commission (<0>last {{count}} epochs</0>)"
values={{
count: aggregationEpochs,
}}
components={[
<Tooltip
key="0"
description={t(
'Depending on data node retention you may not be able see the full 30 days'
)}
>
<span>last 30 epochs</span>
</Tooltip>,
]}
/>
}
description={<QUSDTooltip />}
>
<Value data={totalCommission} formatter={compactFormatter(0)} />
</StatTile>
);
};
export const RefereesTile = ({
referees,
}: {
referees: ReferrerStats['referees'];
}) => {
const t = useT();
return (
<StatTile title={t('Number of traders')} testId="number-of-traders">
<Value data={referees} formatter={valueFormatter(t('None'))} />
</StatTile>
);
};
/* Referee tiles */
export const BenefitTierTile = ({
benefitTier,
nextBenefitTier,
}: {
benefitTier: RefereeStats['benefitTier'];
nextBenefitTier: RefereeStats['nextBenefitTier'];
}) => {
const t = useT();
const formatter = (value: BenefitTier | undefined) =>
value?.tier || t('None');
const next = nextBenefitTier.value?.tier;
return (
<StatTile
title={t('Current tier')}
testId="current-tier"
description={
next
? t('(Next tier: {{nextTier}})', {
nextTier: next,
})
: undefined
}
>
<Value<BenefitTier | undefined>
data={benefitTier}
formatter={formatter}
/>
</StatTile>
);
};
export const RunningVolumeTile = ({
runningVolume,
aggregationEpochs,
}: {
runningVolume: RefereeStats['runningVolume'];
aggregationEpochs: number;
}) => {
const t = useT();
return (
<StatTile
title={t(
'runningNotionalOverEpochs',
'Combined volume (last {{count}} epochs)',
{
count: aggregationEpochs,
}
)}
testId="combined-volume"
>
<Value data={runningVolume} formatter={compactFormatter(2)} />
</StatTile>
);
};
export const DiscountTile = ({
discountFactor,
}: {
discountFactor: RefereeStats['discountFactor'];
}) => {
const t = useT();
return (
<StatTile title={t('Discount')} testId="discount">
<Value data={discountFactor} formatter={percentageFormatter} />
</StatTile>
);
};
export const NextTierVolumeTile = ({
runningVolume,
nextBenefitTier,
}: {
runningVolume: RefereeStats['runningVolume'];
nextBenefitTier: RefereeStats['nextBenefitTier'];
}) => {
const t = useT();
const data = {
loading: runningVolume.loading || nextBenefitTier.loading,
error: runningVolume.error || nextBenefitTier.error,
value: [runningVolume.value, nextBenefitTier.value] as [
BigNumber,
BenefitTier | undefined
],
};
const formatter = ([runningVolume, nextBenefitTier]: [
BigNumber,
BenefitTier | undefined
]) => {
if (!nextBenefitTier) return '0';
const volume = BigNumber(nextBenefitTier.minimumVolume).minus(
runningVolume
);
if (volume.isNaN() || volume.isLessThan(0)) return '0';
return compactFormatter(0)(volume);
};
return (
<StatTile title={t('Volume to next tier')} testId="vol-to-next-tier">
<Value<[BigNumber, BenefitTier | undefined]>
data={data}
formatter={formatter}
/>
</StatTile>
);
};
export const EpochsTile = ({ epochs }: { epochs: RefereeStats['epochs'] }) => {
const t = useT();
return (
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
<Value data={epochs} formatter={valueFormatter(t('None'))} />
</StatTile>
);
};
export const NextTierEpochsTile = ({
epochs,
nextBenefitTier,
}: {
epochs: RefereeStats['epochs'];
nextBenefitTier: RefereeStats['nextBenefitTier'];
}) => {
const t = useT();
const data = {
value: [epochs.value, nextBenefitTier.value] as [
BigNumber,
BenefitTier | undefined
],
loading: epochs.loading || nextBenefitTier.loading,
error: epochs.error || nextBenefitTier.error,
};
const formatter = ([epochs, nextBenefitTier]: [
BigNumber,
BenefitTier | undefined
]) => {
if (!nextBenefitTier) return '-';
const value = BigNumber(nextBenefitTier.epochs).minus(epochs);
if (value.isLessThan(0)) {
return '0';
}
return value.toString(10);
};
return (
<StatTile title={t('Epochs to next tier')} testId="epochs-to-next-tier">
<Value data={data} formatter={formatter} />
</StatTile>
);
};
/* Additional settings */
/**
* A list for tiles that should be replaced with `NoProgramTile`
* when the referral program is not set.
*/
const NO_PROGRAM_TILES = {
[BaseCommissionTile.name]: 'Base commission rate',
[StakingMultiplierTile.name]: 'Staking multiplier',
[FinalCommissionTile.name]: 'Final commission rate',
[VolumeTile.name]: 'My volume',
[BenefitTierTile.name]: 'Current tier',
[DiscountTile.name]: 'Discount',
[RunningVolumeTile.name]: 'Combined volume',
[NextTierEpochsTile.name]: 'Epochs to next tier',
[NextTierVolumeTile.name]: 'Volume to next tier',
};
export const NoProgramTileFor = ({ tile }: { tile: string }) => {
const t = useT();
if (Object.keys(NO_PROGRAM_TILES).includes(tile)) {
return <NoProgramTile title={t(NO_PROGRAM_TILES[tile])} />;
}
return null;
};
/** Teams */
export const TeamTile = ({ teamId }: { teamId?: string }) => {
const { team, members } = useTeam(teamId);
const { data: games } = useGames(teamId);
if (!team) return null;
return (
<Tile className="flex gap-3 lg:gap-4">
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
<div className="flex flex-col items-start gap-1 lg:gap-3">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
<TeamStats
members={members}
games={areTeamGames(games) ? games : undefined}
/>
</div>
</Tile>
);
};

View File

@ -1,126 +1,63 @@
import type { InMemoryCacheConfig } from '@apollo/client';
import {
AppFailure,
AppLoader,
NetworkLoader,
NodeFailure,
NodeGuard,
useEnvironment,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { useEffect, type ReactNode, useState } from 'react';
import { type ReactNode } from 'react';
import { Web3Provider } from './web3-provider';
import { useT } from '../../lib/use-t';
import { DataLoader } from './data-loader';
import { WalletProvider } from '@vegaprotocol/wallet-react';
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 }) => {
const t = useT();
const { error, VEGA_URL } = useEnvironment((state) => ({
error: state.error,
VEGA_URL: state.VEGA_URL,
}));
const { error, VEGA_URL } = useEnvironment();
const config = useVegaWalletConfig();
if (!config) {
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 (
<NetworkLoader
cache={cacheConfig}
skeleton={<Loading />}
failure={<Failure reason={error} />}
skeleton={<AppLoader />}
failure={
<AppFailure title={t('Could not initialize app')} error={error} />
}
>
<DataLoader
skeleton={<Loading />}
failure={<Failure reason={ERR_DATA_LOADER} />}
<NodeGuard
skeleton={<AppLoader />}
failure={
<NodeFailure
title={t('Node: {{VEGA_URL}} is unsuitable', { VEGA_URL })}
/>
}
>
<Web3Provider
skeleton={<Loading />}
failure={<Failure reason={t('Could not configure web3 provider')} />}
<DataLoader
skeleton={<AppLoader />}
failure={
<AppFailure
title={t('Could not load market data or asset data')}
error={error}
/>
}
>
<WalletProvider config={config}>{children}</WalletProvider>
</Web3Provider>
</DataLoader>
<Web3Provider
skeleton={<AppLoader />}
failure={
<AppFailure title={t('Could not configure web3 provider')} />
}
>
<WalletProvider config={config}>{children}</WalletProvider>
</Web3Provider>
</DataLoader>
</NodeGuard>
</NetworkLoader>
);
};
@ -170,9 +107,6 @@ const cacheConfig: InMemoryCacheConfig = {
Fees: {
keyFields: false,
},
PartyProfile: {
keyFields: ['partyId'],
},
// The folling types are cached by the data provider and not by apollo
PositionUpdate: {
keyFields: false,

View File

@ -30,6 +30,8 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
setStudies,
setStudySizes,
setOverlays,
state,
setState,
} = useChartSettings();
const pennantChart = (
@ -66,6 +68,10 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
onIntervalChange={(newInterval) => {
setInterval(fromTradingViewResolution(newInterval));
}}
onAutoSaveNeeded={(data) => {
setState(data);
}}
state={state}
/>
);
}

View File

@ -9,6 +9,7 @@ type StudySizes = { [S in Study]?: number };
export type Chartlib = 'pennant' | 'tradingview';
interface StoredSettings {
state: object | undefined; // Don't see a better type provided from TradingView type definitions
chartlib: Chartlib;
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
// chart types easier and more consistent
@ -29,6 +30,7 @@ const STUDY_ORDER: Study[] = [
];
export const DEFAULT_CHART_SETTINGS = {
state: undefined,
chartlib: 'pennant' as const,
interval: Interval.INTERVAL_I15M,
type: ChartType.CANDLE,
@ -45,6 +47,7 @@ export const useChartSettingsStore = create<
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
setChartlib: (lib: Chartlib) => void;
setState: (state: object) => void;
}
>()(
persist(
@ -92,6 +95,9 @@ export const useChartSettingsStore = create<
state.chartlib = lib;
});
},
setState: (state) => {
set({ state });
},
})),
{
name: 'vega_candles_chart_store',
@ -145,5 +151,7 @@ export const useChartSettings = () => {
setOverlays: settings.setOverlays,
setStudySizes: settings.setStudySizes,
setChartlib: settings.setChartlib,
state: settings.state,
setState: settings.setState,
};
};

View File

@ -8,12 +8,6 @@ import {
mockConfig,
MockedWalletProvider,
} 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', () => ({
ProtocolUpgradeCountdown: () => null,
@ -30,45 +24,15 @@ describe('Navbar', () => {
publicKey: '2'.repeat(64),
},
];
const key1Alias = 'key 1 alias';
const marketId = 'abc';
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[]) => {
return render(
<MemoryRouter initialEntries={initialEntries}>
<MockedProvider mocks={[partyProfilesMock]}>
<MockedWalletProvider>
<Navbar />
</MockedWalletProvider>
</MockedProvider>
<MockedWalletProvider>
<Navbar />
</MockedWalletProvider>
</MemoryRouter>
);
};
@ -176,7 +140,6 @@ describe('Navbar', () => {
const activeKey = within(menu.getByTestId(/key-1+-mobile/));
expect(activeKey.getByText(mockKeys[0].name)).toBeInTheDocument();
expect(activeKey.getByTestId('icon-tick')).toBeInTheDocument();
expect(screen.getByText(key1Alias)).toBeInTheDocument();
const inactiveKey = within(menu.getByTestId(/key-2+-mobile/));
await userEvent.click(inactiveKey.getByText(mockKeys[1].name));

View File

@ -31,27 +31,39 @@ export const ProfileDialog = () => {
const pubKey = useProfileDialogStore((store) => store.pubKey);
const setOpen = useProfileDialogStore((store) => store.setOpen);
const { send, status, error, reset } = useSimpleTransaction({
onSuccess: () => {
refetch();
},
});
const profileEdge = data?.partiesProfilesConnection?.edges.find(
(e) => e.node.partyId === pubKey
);
const sendTx = (field: FormFields) => {
send({
updatePartyProfile: {
alias: field.alias,
metadata: [],
},
});
};
return (
<Dialog
open={open}
onChange={() => {
setOpen(undefined);
reset();
}}
title={t('Edit profile')}
>
<ProfileFormContainer
<ProfileForm
profile={profileEdge?.node}
onSuccess={() => {
refetch();
setTimeout(() => {
setOpen(undefined);
}, 1000);
}}
status={status}
error={error}
onSubmit={sendTx}
/>
</Dialog>
);
@ -65,32 +77,6 @@ type Profile = NonNullable<
PartyProfilesQuery['partiesProfilesConnection']
>['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 = ({
profile,
onSubmit,
@ -128,14 +114,6 @@ const ProfileForm = ({
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 (
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
<FormGroup label="Alias" labelFor="alias">
@ -153,6 +131,12 @@ const ProfileForm = ({
</p>
</InputError>
)}
{status === 'confirmed' && (
<p className="mt-2 mb-4 text-sm text-success">
{t('Profile updated')}
</p>
)}
</FormGroup>
<TradingButton
type="submit"

View File

@ -94,7 +94,6 @@ export const VegaWalletConnectButton = ({
<KeypairRadioGroup
pubKey={pubKey}
pubKeys={pubKeys}
activeKey={activeKey?.publicKey}
onSelect={selectPubKey}
/>
<TradingDropdownSeparator />
@ -139,18 +138,15 @@ export const VegaWalletConnectButton = ({
const KeypairRadioGroup = ({
pubKey,
pubKeys,
activeKey,
onSelect,
}: {
pubKey: string | undefined;
pubKeys: Key[];
activeKey: string | undefined;
onSelect: (pubKey: string) => void;
}) => {
const { data } = usePartyProfilesQuery({
variables: { partyIds: pubKeys.map((pk) => pk.publicKey) },
skip: pubKeys.length <= 0,
fetchPolicy: 'cache-and-network',
});
return (
@ -160,27 +156,14 @@ const KeypairRadioGroup = ({
(e) => e.node.partyId === pk.publicKey
);
return (
<KeypairItem
key={pk.publicKey}
pk={pk}
isActive={activeKey === pk.publicKey}
alias={profile?.node.alias}
/>
<KeypairItem key={pk.publicKey} pk={pk} alias={profile?.node.alias} />
);
})}
</TradingDropdownRadioGroup>
);
};
const KeypairItem = ({
pk,
isActive,
alias,
}: {
pk: Key;
alias: string | undefined;
isActive: boolean;
}) => {
const KeypairItem = ({ pk, alias }: { pk: Key; alias: string | undefined }) => {
const t = useT();
const [copied, setCopied] = useCopyTimeout();
const setOpen = useProfileDialogStore((store) => store.setOpen);
@ -211,13 +194,8 @@ const KeypairItem = ({
data-testid={`key-${pk.publicKey}`}
>
<Tooltip description={t('Public facing key alias. Click to edit')}>
<button
data-testid="alias"
onClick={() => setOpen(pk.publicKey)}
className="flex items-center gap-1"
>
<button data-testid="alias" onClick={() => setOpen(pk.publicKey)}>
{alias ? alias : t('No alias')}
{isActive && <VegaIcon name={VegaIconNames.EDIT} />}
</button>
</Tooltip>
</div>

View File

@ -12,8 +12,6 @@ import CopyToClipboard from 'react-copy-to-clipboard';
import { ViewType, useSidebar } from '../sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
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 = ({
setMenu,
@ -25,12 +23,6 @@ export const VegaWalletMenu = ({
const currentRouteId = useGetCurrentRouteId();
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(() => {
return pubKeys?.find((pk) => pk.publicKey === pubKey);
}, [pubKey, pubKeys]);
@ -45,21 +37,14 @@ export const VegaWalletMenu = ({
return (
<div>
<div className="grow my-4" role="list">
{(pubKeys || []).map((pk) => {
const profile = data?.partiesProfilesConnection?.edges.find(
(e) => e.node.partyId === pk.publicKey
);
return (
<KeypairListItem
key={pk.publicKey}
pk={pk}
isActive={activeKey?.publicKey === pk.publicKey}
onSelectItem={onSelectItem}
alias={profile?.node.alias}
setMenu={setMenu}
/>
);
})}
{(pubKeys || []).map((pk) => (
<KeypairListItem
key={pk.publicKey}
pk={pk}
isActive={activeKey?.publicKey === pk.publicKey}
onSelectItem={onSelectItem}
/>
))}
</div>
<div className="flex flex-col gap-2 m-4">
@ -87,23 +72,18 @@ export const VegaWalletMenu = ({
const KeypairListItem = ({
pk,
isActive,
alias,
onSelectItem,
setMenu,
}: {
pk: Key;
isActive: boolean;
alias: string | undefined;
onSelectItem: (pk: string) => void;
setMenu: (open: 'nav' | 'wallet' | null) => void;
}) => {
const t = useT();
const [copied, setCopied] = useCopyTimeout();
const setOpen = useProfileDialogStore((store) => store.setOpen);
return (
<div
className="flex flex-col w-full px-4 mb-4"
className="flex flex-col w-full ml-4 mr-2 mb-4"
data-testid={`key-${pk.publicKey}-mobile`}
>
<span className="flex gap-2 items-center mr-2">
@ -126,24 +106,6 @@ const KeypairListItem = ({
</CopyToClipboard>
{copied && <span className="text-xs">{t('Copied')}</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>
);
};

Some files were not shown because too many files have changed in this diff Show More