Compare commits

..

9 Commits

Author SHA1 Message Date
asiaznik
002738af70
chore: skip picking if VEGA_URL set 2024-03-08 14:11:42 +01:00
asiaznik
b0bbae4c2e
chore: skip picking if VEGA_URL set 2024-03-08 13:53:57 +01:00
asiaznik
635445f23b
chore: clear timeout 2024-03-08 13:53:57 +01:00
asiaznik
10e30b9f70
chore: move copy 2024-03-08 13:53:57 +01:00
asiaznik
46c6e27aec
chore: remove app failure component 2024-03-08 13:53:57 +01:00
asiaznik
4bd73bfc0f
chore: add np to mocks 2024-03-08 13:53:57 +01:00
asiaznik
15aad9db62
chore: add np to mocks 2024-03-08 13:53:57 +01:00
asiaznik
46308b381f
chore: i18n texts 2024-03-08 13:53:57 +01:00
asiaznik
938ad08a60
fix(trading): pick the best node to connect, slow notificatio, init error 2024-03-08 13:53:57 +01:00
183 changed files with 2755 additions and 1696 deletions

View File

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

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

@ -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

@ -170,9 +170,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

@ -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>
);
};

View File

@ -9,14 +9,12 @@ from actions.utils import next_epoch, change_keys
from wallet_config import MM_WALLET
from vega_sim.null_service import VegaServiceNull
@pytest.fixture(scope="module")
def setup_environment(request, browser) -> Generator[Tuple[Page, str, str], None, None]:
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance))
tDAI_market, tDAI_asset_id = setup_market_with_reward_program(
vega_instance)
tDAI_market, tDAI_asset_id = setup_market_with_reward_program(vega_instance)
with init_page(vega_instance, browser, request) as page:
risk_accepted_setup(page)
@ -149,12 +147,14 @@ def setup_market_with_reward_program(vega: VegaServiceNull):
return tDAI_market, tDAI_asset_id
def test_network_reward_pot(setup_environment: Tuple[Page, str, str],
) -> None:
def test_network_reward_pot( setup_environment: Tuple[Page, str, str],
) -> None:
page, tDAI_market, tDAI_asset_id = setup_environment
expect(page.get_by_test_id(TOTAL_REWARDS)).to_have_text("183.33333 tDAI")
def test_reward_multiplier(
setup_environment: Tuple[Page, str, str],
) -> None:
@ -168,6 +168,7 @@ def test_reward_multiplier(
)
def test_reward_history(
setup_environment: Tuple[Page, str, str],
) -> None:
@ -176,37 +177,26 @@ def test_reward_history(
expect((page.get_by_role(ROW).locator(PRICE_TAKING_COL_ID)).nth(1)).to_have_text(
"299.99999100.00%"
)
expect((page.get_by_role(ROW).locator(TOTAL_COL_ID)).nth(
1)).to_have_text("299.99999")
expect((page.get_by_role(ROW).locator(TOTAL_COL_ID)).nth(1)).to_have_text("299.99999")
page.get_by_test_id(EARNED_BY_ME_BUTTON).click()
expect((page.get_by_role(ROW).locator(TOTAL_COL_ID)).nth(1)).to_have_text(
"183.33333"
)
def test_staking_reward(
setup_environment: Tuple[Page, str, str],
page: Page,
):
page, tDAI_market, tDAI_asset_id = setup_environment
expect(page.get_by_test_id("active-rewards-card")).to_have_count(2)
staking_reward_card = page.get_by_test_id("active-rewards-card").nth(1)
expect(staking_reward_card).to_be_visible()
expect(staking_reward_card.get_by_test_id(
"entity-scope")).to_have_text("Individual")
expect(staking_reward_card.get_by_test_id(
"locked-for")).to_have_text("0 epochs")
expect(staking_reward_card.get_by_test_id(
"reward-value")).to_have_text("100.00")
expect(staking_reward_card.get_by_test_id(
"distribution-strategy")).to_have_text("Pro rata")
expect(staking_reward_card.get_by_test_id("entity-scope")).to_have_text("Individual")
expect(staking_reward_card.get_by_test_id("locked-for")).to_have_text("0 epochs")
expect(staking_reward_card.get_by_test_id("reward-value")).to_have_text("100.00")
expect(staking_reward_card.get_by_test_id("distribution-strategy")).to_have_text("Pro rata")
expect(staking_reward_card.get_by_test_id("dispatch-metric-info")).to_have_text(
"Staking rewards"
)
expect(staking_reward_card.get_by_test_id(
"assessed-over")).to_have_text("1 epoch")
expect(staking_reward_card.get_by_test_id(
"scope")).to_have_text("Individual")
expect(staking_reward_card.get_by_test_id(
"staking-requirement")).to_have_text("1.00")
expect(staking_reward_card.get_by_test_id(
"average-position")).to_have_text("0.00")
expect(staking_reward_card.get_by_test_id("assessed-over")).to_have_text("1 epoch")
expect(staking_reward_card.get_by_test_id("scope")).to_have_text("Individual")
expect(staking_reward_card.get_by_test_id("staking-requirement")).to_have_text("1.00")
expect(staking_reward_card.get_by_test_id("average-position")).to_have_text("0.00")

View File

@ -39,8 +39,6 @@ const nextConfig = {
GIT_COMMIT: commitHash,
GIT_TAG: tag,
},
basePath: '/apps/vegas', // Set the base path
assetPrefix: '/apps/vegas', // Set the asset prefix
};
module.exports = SENTRY_AUTH_TOKEN

View File

@ -4,10 +4,11 @@ export default function Document() {
return (
<>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
{/*
meta tags
- next advised against using _document for this, so they exist in our
- single page index.page.tsx
*/}
{/* preload fonts */}
<link
@ -17,38 +18,15 @@ export default function Document() {
type="font/woff"
/>
<link
rel="preload"
href="/AlphaLyrae.woff2"
as="font"
type="font/woff2"
/>
{/* icons */}
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="apple-touch-icon" content="/favicon.ico" />
{/* scripts */}
<script src="/theme-setter.js" type="text/javascript" async />
{/* manifest */}
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="/apps/trading/public/manifest.json" />
</Head>
<Html>
<body

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -9,14 +9,14 @@
"background_color": "#ffffff",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
"src": "cover.png",
"type": "image/png",
"sizes": "192x192"
}
]
}

View File

@ -1,9 +1,7 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nx/next/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

@ -1,4 +1,4 @@
import { getJestProjects } from '@nx/jest';
const { getJestProjects } = require('@nx/jest');
export default {
projects: getJestProjects(),

View File

@ -19,6 +19,7 @@ import {
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react';
import { useCallback, useEffect, useState } from 'react';
@ -26,7 +27,6 @@ import { Controller, useForm } from 'react-hook-form';
import { AssetOption, Balance } from '@vegaprotocol/assets';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { useTransferFeeQuery } from './__generated__/TransferFee';
import { normalizeTransfer } from './utils';
interface FormFields {
toVegaKey: string;

View File

@ -1,26 +0,0 @@
import type { Exact } from 'type-fest';
import { type Transfer } from '@vegaprotocol/wallet';
import { removeDecimal } from '@vegaprotocol/utils';
import { type AccountType } from '@vegaprotocol/types';
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
address: string,
amount: string,
fromAccountType: AccountType,
toAccountType: AccountType,
asset: {
id: string;
decimals: number;
}
): Transfer => {
return {
to: address,
fromAccountType,
toAccountType,
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
// oneOff or recurring required otherwise wallet will error
// default oneOff is immediate transfer
oneOff: {},
};
};

View File

@ -1,9 +1,10 @@
import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { determinePriceStep, useValidateAmount } from '@vegaprotocol/utils';
import { toDecimal, useValidateAmount } from '@vegaprotocol/utils';
import {
TradingFormGroup,
TradingInputError,
Tooltip,
FormGroup,
Input,
@ -29,7 +30,31 @@ export const DealTicketPriceTakeProfitStopLoss = ({
}: DealTicketPriceTakeProfitStopLossProps) => {
const t = useT();
const validateAmount = useValidateAmount();
const priceStep = determinePriceStep(market);
const priceStep = toDecimal(market?.decimalPlaces);
const renderTakeProfitError = () => {
if (takeProfitError) {
return (
<TradingInputError testId="deal-ticket-take-profit-error-message">
{takeProfitError}
</TradingInputError>
);
}
return null;
};
const renderStopLossError = () => {
if (stopLossError) {
return (
<TradingInputError testId="deal-stop-loss-error-message">
{stopLossError}
</TradingInputError>
);
}
return null;
};
return (
<div className="mb-2">
@ -79,9 +104,9 @@ export const DealTicketPriceTakeProfitStopLoss = ({
{...field}
/>
</FormGroup>
{(fieldState.error || takeProfitError) && (
{fieldState.error && (
<InputError testId="deal-ticket-error-message-price-take-profit">
{fieldState.error?.message || takeProfitError}
{fieldState.error.message}
</InputError>
)}
</div>
@ -129,9 +154,9 @@ export const DealTicketPriceTakeProfitStopLoss = ({
{...field}
/>
</FormGroup>
{(fieldState.error || stopLossError) && (
{fieldState.error && (
<InputError testId="deal-ticket-error-message-price-stop-loss">
{fieldState.error?.message || stopLossError}
{fieldState.error.message}
</InputError>
)}
</div>
@ -140,6 +165,8 @@ export const DealTicketPriceTakeProfitStopLoss = ({
</TradingFormGroup>
</div>
</div>
{renderTakeProfitError()}
{renderStopLossError()}
</div>
);
};

View File

@ -1,7 +1,7 @@
import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { useValidateAmount } from '@vegaprotocol/utils';
import { toDecimal, useValidateAmount } from '@vegaprotocol/utils';
import {
TradingFormGroup,
TradingInput,
@ -9,7 +9,6 @@ import {
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { useT } from '../../use-t';
import { determineSizeStep } from '@vegaprotocol/utils';
export interface DealTicketSizeIcebergProps {
control: Control<OrderFormValues>;
@ -30,7 +29,7 @@ export const DealTicketSizeIceberg = ({
}: DealTicketSizeIcebergProps) => {
const t = useT();
const validateAmount = useValidateAmount();
const sizeStep = determineSizeStep(market);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const renderPeakSizeError = () => {
if (peakSizeError) {

View File

@ -8,6 +8,7 @@ import {
formatForInput,
formatValue,
removeDecimal,
toDecimal,
useValidateAmount,
} from '@vegaprotocol/utils';
import { type Control, type UseFormWatch } from 'react-hook-form';
@ -58,7 +59,6 @@ import { KeyValue } from './key-value';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { stopOrdersProvider } from '@vegaprotocol/orders';
import { useT } from '../../use-t';
import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils';
export interface StopOrderProps {
market: Market;
@ -904,8 +904,8 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
market.tradableInstrument.instrument.metadata.tags
);
const sizeStep = determineSizeStep(market);
const priceStep = determinePriceStep(market);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const priceStep = toDecimal(market?.decimalPlaces);
useController({
name: 'type',

View File

@ -883,7 +883,7 @@ describe('DealTicket', () => {
'deal-ticket-error-message-price'
);
expect(errorMessage).toHaveTextContent(
'Price must be a multiple of 0.01 for this market'
'Price accepts up to 2 decimal places'
);
});
});

View File

@ -32,6 +32,7 @@ import {
toBigNum,
removeDecimal,
useValidateAmount,
toDecimal,
formatForInput,
formatValue,
} from '@vegaprotocol/utils';
@ -81,7 +82,6 @@ import { DocsLinks } from '@vegaprotocol/environment';
import { useT } from '../../use-t';
import { DealTicketPriceTakeProfitStopLoss } from './deal-ticket-price-tp-sl';
import uniqueId from 'lodash/uniqueId';
import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils';
export const REDUCE_ONLY_TOOLTIP =
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
@ -419,12 +419,11 @@ export const DealTicket = ({
},
});
const sizeStep = determineSizeStep(market);
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName = getQuoteName(market);
const isLimitType = type === Schema.OrderType.TYPE_LIMIT;
const priceStep = determinePriceStep(market);
return (
<form
onSubmit={

View File

@ -11,7 +11,6 @@ export function generateMarket(override?: PartialDeep<Market>): Market {
positionDecimalPlaces: 1,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
state: Schema.MarketState.STATE_ACTIVE,
tickSize: '1',
marketTimestamps: {
__typename: 'MarketTimestamps',
close: '',

View File

@ -20,11 +20,5 @@
"jest.config.ts",
"__mocks__"
],
"include": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx",
"../utils/src/lib/step.ts"
]
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@ -54,7 +54,6 @@ export const generateFill = (override?: PartialDeep<Trade>) => {
decimalPlaces: 5,
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
tickSize: '1',
fees: {
__typename: 'Fees',
factors: {

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