Feat/lp dashboard markets (#1664)
* feat: generate new nx application * feat: add env variables & render a headline * feat: add cypress projectId and delete unused files * feat: liquidity market list * feat: render LP grid * feat: create liquidity provision lib * feat: liquidity provision calculate volume * feat: add volume change, generate types * feat: add EnvironmentProvider * feat: add LP health bar * feat: liquidity provision health * feat: liquidity provider dashboard healthbars * feat: liquidity provider dashnoard - add auction trigger * feat: liquidity provider dashboard - display multiple fees * feat: liquidity provision provider refactor * feat: liquidity provision provider refactor * feat: liquidity utils tests * feat: add benefits and links to docs * feat: liquidiity provision styles * feat: fix liquidity provision e2e tests * feat: liquidity target * feat: liquidity provision dashboard
This commit is contained in:
parent
1124a86527
commit
3b41e9f2f8
27
apps/liquidity-provision-dashboard-e2e/.env
Normal file
27
apps/liquidity-provision-dashboard-e2e/.env
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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://static.vega.xyz/assets/testnet-network.json"
|
||||||
|
NX_VEGA_ENV = 'TESTNET'
|
||||||
|
NX_VEGA_URL="https://api.n11.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={"MAINNET":"https://alpha.console.vega.xyz"}
|
||||||
|
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
3
apps/liquidity-provision-dashboard-e2e/.env.capsule
Normal file
3
apps/liquidity-provision-dashboard-e2e/.env.capsule
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# App configuration variables
|
||||||
|
NX_VEGA_URL=http://localhost:3028/query
|
||||||
|
NX_VEGA_ENV=LOCAL
|
8
apps/liquidity-provision-dashboard-e2e/.env.devnet
Normal file
8
apps/liquidity-provision-dashboard-e2e/.env.devnet
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# App configuration variables
|
||||||
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||||
|
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
|
||||||
|
NX_VEGA_ENV=DEVNET
|
||||||
|
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
|
||||||
|
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_EXPLORER_URL=https://dev.explorer.vega.xyz
|
8
apps/liquidity-provision-dashboard-e2e/.env.mainnet
Normal file
8
apps/liquidity-provision-dashboard-e2e/.env.mainnet
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# App configuration variables
|
||||||
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
|
||||||
|
NX_VEGA_URL=https://api.vega.xyz/query
|
||||||
|
NX_VEGA_ENV=MAINNET
|
||||||
|
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
||||||
|
NX_ETHEREUM_PROVIDER_URL=https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
|
NX_ETHERSCAN_URL=https://etherscan.io
|
||||||
|
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
|
7
apps/liquidity-provision-dashboard-e2e/.env.stagnet3
Normal file
7
apps/liquidity-provision-dashboard-e2e/.env.stagnet3
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# App configuration variables
|
||||||
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||||
|
NX_VEGA_URL=https://api.n01.stagnet3.vega.xyz/graphql
|
||||||
|
NX_VEGA_ENV=STAGNET3
|
||||||
|
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||||
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_EXPLORER_URL=https://staging2.explorer.vega.xyz
|
8
apps/liquidity-provision-dashboard-e2e/.env.testnet
Normal file
8
apps/liquidity-provision-dashboard-e2e/.env.testnet
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# App configuration variables
|
||||||
|
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
|
||||||
|
NX_VEGA_URL=https://api.n11.testnet.vega.xyz/graphql
|
||||||
|
NX_VEGA_ENV=TESTNET
|
||||||
|
NX_VEGA_NETWORKS='{\"MAINNET\":\"https://alpha.console.vega.xyz\"}'
|
||||||
|
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
|
@ -4,10 +4,6 @@ module.exports = defineConfig({
|
|||||||
projectId: 'et4snf',
|
projectId: 'et4snf',
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
require('cypress-grep/src/plugin')(config);
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
baseUrl: 'http://localhost:4200',
|
baseUrl: 'http://localhost:4200',
|
||||||
fileServerFolder: '.',
|
fileServerFolder: '.',
|
||||||
fixturesFolder: false,
|
fixturesFolder: false,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { getGreeting } from '../support/app.po';
|
describe('liquidity-provision-dashboard', () => {
|
||||||
|
|
||||||
describe('liquidity-provision-dashboard', { tags: '@smoke' }, () => {
|
|
||||||
beforeEach(() => cy.visit('/'));
|
beforeEach(() => cy.visit('/'));
|
||||||
|
|
||||||
|
it('render', () => {
|
||||||
|
cy.get('#root').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
it('should display welcome message', () => {
|
it('should display welcome message', () => {
|
||||||
// Function helper example, see `../support/app.po.ts` file
|
cy.get('h1').contains('Top liquidity opportunities');
|
||||||
getGreeting().contains('Top liquidity opportunities');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,5 +17,3 @@ import '@vegaprotocol/cypress';
|
|||||||
import 'cypress-real-events/support';
|
import 'cypress-real-events/support';
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands';
|
import './commands';
|
||||||
import registerCypressGrep from 'cypress-grep';
|
|
||||||
registerCypressGrep();
|
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import '../styles.scss';
|
import '../styles.scss';
|
||||||
|
import { Header } from './components/header';
|
||||||
|
import { Intro } from './components/intro';
|
||||||
|
import { MarketList } from './components/market-list';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-stretch px-6 py-6">
|
<div className="max-h-full min-h-full bg-white">
|
||||||
<h1 className="text-3xl">Top liquidity opportunities</h1>
|
<Header />
|
||||||
|
<Intro />
|
||||||
|
<MarketList />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
export const Header = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-stretch px-6 py-6" data-testid="header">
|
||||||
|
<h1 className="text-3xl">{t('Top liquidity opportunities')}</h1>
|
||||||
|
<div className="flex items-center gap-2 ml-auto relative z-10">
|
||||||
|
{t('Network switcher')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './header';
|
@ -0,0 +1 @@
|
|||||||
|
export * from './intro';
|
@ -0,0 +1,54 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
// TODO: add mainnet links once docs have been updated
|
||||||
|
const LINKS = {
|
||||||
|
testnet: [
|
||||||
|
{
|
||||||
|
label: 'Understand how liquidity fees are calculated',
|
||||||
|
url: 'https://docs.vega.xyz/docs/testnet/tutorials/providing-liquidity#resources',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'How to provide liquidity',
|
||||||
|
url: 'https://docs.vega.xyz/docs/testnet/tutorials/providing-liquidity#overview',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'How to view existing liquidity provisions',
|
||||||
|
url: 'https://docs.vega.xyz/docs/testnet/tutorials/providing-liquidity#viewing-existing-liquidity-provisions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'How to amend or remove liquidity',
|
||||||
|
url: 'https://docs.vega.xyz/docs/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 className="mx-6 my-6 px-6 py-6 bg-neutral-100" data-testid="intro">
|
||||||
|
<h2 className="text-xl font-medium mb-1">
|
||||||
|
{t('Become a liquidity provider')}
|
||||||
|
</h2>
|
||||||
|
<p className="text-base mb-2">
|
||||||
|
{t('Earn a cut of the fees paid by price takers 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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,207 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
|
import { t, addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||||
|
import { BigNumber } from 'bignumber.js';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const marketTradingModeStyle = {
|
||||||
|
[MarketTradingMode.TRADING_MODE_CONTINUOUS]: '#00a88a',
|
||||||
|
[MarketTradingMode.TRADING_MODE_MONITORING_AUCTION]: '#fb8e7f',
|
||||||
|
[MarketTradingMode.TRADING_MODE_OPENING_AUCTION]: '#68e2e4',
|
||||||
|
[MarketTradingMode.TRADING_MODE_BATCH_AUCTION]: 'batch',
|
||||||
|
[MarketTradingMode.TRADING_MODE_NO_TRADING]: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COPY_CLASS = 'text-[8px] leading-[1.2em] font-medium';
|
||||||
|
|
||||||
|
const Tooltip = ({
|
||||||
|
children,
|
||||||
|
isExpanded,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
isExpanded: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'absolute top-0 left-1/2 -translate-x-2/4 -translate-y-[120%] border border-[#bfccd6] py-0.5 px-2 flex-col z-10 bg-white group-hover:flex min-w-[65px]',
|
||||||
|
{
|
||||||
|
flex: isExpanded,
|
||||||
|
hidden: !isExpanded,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute w-0 h-0 translate-y-full translate-x-2/4 left-[calc(50% - 8px)] -bottom-px border-4"
|
||||||
|
style={{
|
||||||
|
left: 'calc(50% - 8px)',
|
||||||
|
borderColor: '#bfccd6 transparent transparent transparent',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
left: 'calc(50% - 8px)',
|
||||||
|
borderColor: 'white transparent transparent transparent',
|
||||||
|
}}
|
||||||
|
className="absolute bottom-0 w-0 h-0 translate-y-full translate-x-2/4 left-[calc(50% - 8px)] border-4"
|
||||||
|
></div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Target = ({
|
||||||
|
targetPercent,
|
||||||
|
isLarge,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
targetPercent: number;
|
||||||
|
isLarge: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'absolute top-0 left-1/2 -translate-x-2/4 px-1.5 group'
|
||||||
|
)}
|
||||||
|
style={{ left: `${targetPercent}%` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames('health-target w-0.5 h-8 bg-black', {
|
||||||
|
'h-[72px]': isLarge,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Level = ({
|
||||||
|
children,
|
||||||
|
commitmentAmount,
|
||||||
|
total,
|
||||||
|
index,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
index: number;
|
||||||
|
status: MarketTradingMode;
|
||||||
|
commitmentAmount: number;
|
||||||
|
total: number;
|
||||||
|
}) => {
|
||||||
|
const width = new BigNumber(commitmentAmount)
|
||||||
|
.div(total)
|
||||||
|
.multipliedBy(100)
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(`relative h-[inherit] w-full group`)}
|
||||||
|
style={{
|
||||||
|
width: `${width}%`,
|
||||||
|
opacity: 1 - 0.1 * index,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative w-full h-[inherit]"
|
||||||
|
style={{
|
||||||
|
opacity: 1 - 0.1 * index,
|
||||||
|
backgroundColor: marketTradingModeStyle[status],
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Full = () => (
|
||||||
|
<div className="bg-neutral-100 w-full h-[inherit] absolute bottom-0 left-0"></div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Levels {
|
||||||
|
fee: string;
|
||||||
|
commitmentAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HealthBar = ({
|
||||||
|
status,
|
||||||
|
target,
|
||||||
|
decimals,
|
||||||
|
levels,
|
||||||
|
size = 'small',
|
||||||
|
isExpanded = false,
|
||||||
|
}: {
|
||||||
|
status: MarketTradingMode;
|
||||||
|
target: string;
|
||||||
|
decimals: number;
|
||||||
|
levels: Levels[];
|
||||||
|
isExpanded?: boolean;
|
||||||
|
size?: 'small' | 'large';
|
||||||
|
}) => {
|
||||||
|
const targetNumber = parseInt(target, 10);
|
||||||
|
|
||||||
|
const committedNumber = levels
|
||||||
|
.reduce((total, current) => {
|
||||||
|
return total.plus(current.commitmentAmount);
|
||||||
|
}, new BigNumber(0))
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
const total =
|
||||||
|
targetNumber * 2 >= committedNumber ? targetNumber * 2 : committedNumber;
|
||||||
|
const targetPercent = (targetNumber / total) * 100;
|
||||||
|
const isLarge = size === 'large';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div
|
||||||
|
className={classNames('health-wrapper relative', {
|
||||||
|
'py-2': !isLarge,
|
||||||
|
'py-5': isLarge,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames('health-inner relative w-full flex', {
|
||||||
|
'h-4': !isLarge,
|
||||||
|
'h-8': isLarge,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Full />
|
||||||
|
|
||||||
|
<div className="health-bars h-[inherit] flex w-full">
|
||||||
|
{levels.map((p, index) => {
|
||||||
|
const { commitmentAmount, fee } = p;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Level
|
||||||
|
status={status}
|
||||||
|
commitmentAmount={commitmentAmount}
|
||||||
|
index={index}
|
||||||
|
total={total}
|
||||||
|
>
|
||||||
|
<Tooltip isExpanded={isExpanded}>
|
||||||
|
<span className={COPY_CLASS}>
|
||||||
|
{fee}% {t('Fee')}
|
||||||
|
</span>
|
||||||
|
<span className={COPY_CLASS}>
|
||||||
|
{addDecimalsFormatNumber(commitmentAmount, decimals)}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Level>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Target targetPercent={targetPercent} isLarge={isLarge}>
|
||||||
|
<Tooltip isExpanded={isExpanded}>
|
||||||
|
<span className={COPY_CLASS}>{t('Target stake')}</span>
|
||||||
|
<span className={COPY_CLASS}>
|
||||||
|
{addDecimalsFormatNumber(target, decimals)}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Target>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,100 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { HealthBar } from './health-bar';
|
||||||
|
|
||||||
|
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: 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: 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: 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="medium" open={isOpen} onChange={onChange}>
|
||||||
|
<h1 className="text-xl mb-4 pr-2 font-bold" data-testid="dialog-title">
|
||||||
|
{t('Health')}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl mb-4">
|
||||||
|
{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>
|
||||||
|
<th className="w-1/2 text-left">{t('Market status')}</th>
|
||||||
|
<th className="w-1/2 text-left">{t('Liquidity status')}</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ROWS.map((r) => {
|
||||||
|
return (
|
||||||
|
<tr key={r.key}>
|
||||||
|
<td className="pr-4 py-10">
|
||||||
|
<h2 className="font-bold text-base">{t(r.title)}</h2>
|
||||||
|
<p className="text-base">{t(r.copy)}</p>
|
||||||
|
</td>
|
||||||
|
<td className="py-10">
|
||||||
|
<HealthBar
|
||||||
|
size="large"
|
||||||
|
levels={r.data.levels}
|
||||||
|
status={r.data.status}
|
||||||
|
target={r.data.target}
|
||||||
|
decimals={r.data.decimals}
|
||||||
|
isExpanded
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from './market-list';
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
import { useCallback, useRef, useEffect, useState } from 'react';
|
||||||
|
import { AgGridReact, AgGridColumn } from 'ag-grid-react';
|
||||||
|
import type { AgGridReact as AgGridReactType } from 'ag-grid-react';
|
||||||
|
import type {
|
||||||
|
GroupCellRendererParams,
|
||||||
|
ValueFormatterParams,
|
||||||
|
GetRowIdParams,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
import 'ag-grid-community/dist/styles/ag-grid.css';
|
||||||
|
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
|
||||||
|
import { formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { useMarketsLiquidity } from '@vegaprotocol/liquidity';
|
||||||
|
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { Market } from '@vegaprotocol/liquidity';
|
||||||
|
import { formatWithAsset } from '@vegaprotocol/liquidity';
|
||||||
|
import {
|
||||||
|
MarketTradingModeMapping,
|
||||||
|
MarketTradingMode,
|
||||||
|
AuctionTrigger,
|
||||||
|
AuctionTriggerMapping,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { HealthBar } from './health-bar';
|
||||||
|
import { HealthDialog } from './health-dialog';
|
||||||
|
import './market-list.scss';
|
||||||
|
|
||||||
|
const displayValue = (value: string) => {
|
||||||
|
return parseFloat(value) > 0 ? `+${value}` : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const marketNameCellRenderer = ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
data: Market;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span style={{ lineHeight: '12px' }}>{value}</span>
|
||||||
|
<span style={{ lineHeight: '12px' }}>
|
||||||
|
{data?.tradableInstrument?.instrument?.product?.settlementAsset?.symbol}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const healthCellRenderer = ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
value: MarketTradingMode;
|
||||||
|
data: Market;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HealthBar
|
||||||
|
status={value}
|
||||||
|
target={data.target}
|
||||||
|
decimals={
|
||||||
|
data.tradableInstrument.instrument.product.settlementAsset.decimals
|
||||||
|
}
|
||||||
|
levels={data.feeLevels}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarketList = () => {
|
||||||
|
const { data, error, loading } = useMarketsLiquidity();
|
||||||
|
const [isHealthDialogOpen, setIsHealthDialogOpen] = useState(false);
|
||||||
|
const gridRef = useRef<AgGridReactType | null>(null);
|
||||||
|
|
||||||
|
const getRowId = useCallback(({ data }: GetRowIdParams) => data.id, []);
|
||||||
|
|
||||||
|
const handleOnGridReady = useCallback(() => {
|
||||||
|
gridRef.current?.api?.sizeColumnsToFit();
|
||||||
|
}, [gridRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('resize', handleOnGridReady);
|
||||||
|
return () => window.removeEventListener('resize', handleOnGridReady);
|
||||||
|
}, [handleOnGridReady]);
|
||||||
|
|
||||||
|
if (loading) return <p>Loading...</p>;
|
||||||
|
if (error) return <p>Error :( </p>;
|
||||||
|
|
||||||
|
const localData = data?.markets;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="px-6 py-6 grow"
|
||||||
|
data-testid="market-list"
|
||||||
|
style={{ minHeight: 500, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<AgGridReact
|
||||||
|
rowData={localData}
|
||||||
|
className="ag-theme-alpine h-full"
|
||||||
|
defaultColDef={{
|
||||||
|
resizable: true,
|
||||||
|
sortable: true,
|
||||||
|
unSortIcon: true,
|
||||||
|
cellClass: ['flex', 'flex-col', 'justify-center'],
|
||||||
|
}}
|
||||||
|
getRowId={getRowId}
|
||||||
|
rowHeight={92}
|
||||||
|
ref={gridRef}
|
||||||
|
>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Market (futures)')}
|
||||||
|
field="tradableInstrument.instrument.name"
|
||||||
|
cellRenderer={marketNameCellRenderer}
|
||||||
|
minWidth={100}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Volume (24h)')}
|
||||||
|
field="dayVolume"
|
||||||
|
cellRenderer={({ value, data }: GroupCellRendererParams) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{formatNumber(value)} ({displayValue(data.volumeChange)})
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Committed bond/stake')}
|
||||||
|
field="liquidityCommitted"
|
||||||
|
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
||||||
|
formatWithAsset(
|
||||||
|
value,
|
||||||
|
data.tradableInstrument.instrument.product.settlementAsset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Status')}
|
||||||
|
field="tradingMode"
|
||||||
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
value: MarketTradingMode;
|
||||||
|
data: Market;
|
||||||
|
}) => {
|
||||||
|
return value ===
|
||||||
|
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||||
|
data.data?.trigger &&
|
||||||
|
data.data.trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
|
||||||
|
? `${MarketTradingModeMapping[value]}
|
||||||
|
- ${AuctionTriggerMapping[data.data.trigger]}`
|
||||||
|
: MarketTradingModeMapping[value];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AgGridColumn
|
||||||
|
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={healthCellRenderer}
|
||||||
|
sortable={false}
|
||||||
|
cellStyle={{ overflow: 'unset' }}
|
||||||
|
/>
|
||||||
|
<AgGridColumn headerName={t('Est. return / APY')} field="apy" />
|
||||||
|
</AgGridReact>
|
||||||
|
|
||||||
|
<HealthDialog
|
||||||
|
isOpen={isHealthDialogOpen}
|
||||||
|
onChange={() => {
|
||||||
|
setIsHealthDialogOpen(!isHealthDialogOpen);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,88 @@
|
|||||||
|
import {
|
||||||
|
ApolloClient,
|
||||||
|
from,
|
||||||
|
HttpLink,
|
||||||
|
InMemoryCache,
|
||||||
|
split,
|
||||||
|
} from '@apollo/client';
|
||||||
|
import { onError } from '@apollo/client/link/error';
|
||||||
|
import { RetryLink } from '@apollo/client/link/retry';
|
||||||
|
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
||||||
|
import { createClient as createWSClient } from 'graphql-ws';
|
||||||
|
import { getMainDefinition } from '@apollo/client/utilities';
|
||||||
|
|
||||||
|
export function createClient(base?: string) {
|
||||||
|
if (!base) {
|
||||||
|
throw new Error('Base must be passed into createClient!');
|
||||||
|
}
|
||||||
|
const urlHTTP = new URL(base);
|
||||||
|
const urlWS = new URL(base);
|
||||||
|
// Replace http with ws, preserving if its a secure connection eg. https => wss
|
||||||
|
urlWS.protocol = urlWS.protocol.replace('http', 'ws');
|
||||||
|
|
||||||
|
const cache = new InMemoryCache({
|
||||||
|
typePolicies: {
|
||||||
|
Market: {
|
||||||
|
merge: true,
|
||||||
|
},
|
||||||
|
Party: {
|
||||||
|
merge: true,
|
||||||
|
},
|
||||||
|
Query: {},
|
||||||
|
Account: {
|
||||||
|
keyFields: false,
|
||||||
|
fields: {
|
||||||
|
balanceFormatted: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Node: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
Instrument: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryLink = new RetryLink({
|
||||||
|
delay: {
|
||||||
|
initial: 300,
|
||||||
|
max: 10000,
|
||||||
|
jitter: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpLink = new HttpLink({
|
||||||
|
uri: urlHTTP.href,
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const wsLink = new GraphQLWsLink(
|
||||||
|
createWSClient({
|
||||||
|
url: urlWS.href,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const splitLink = split(
|
||||||
|
({ query }) => {
|
||||||
|
const definition = getMainDefinition(query);
|
||||||
|
return (
|
||||||
|
definition.kind === 'OperationDefinition' &&
|
||||||
|
definition.operation === 'subscription'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
wsLink,
|
||||||
|
httpLink
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||||
|
console.log(graphQLErrors);
|
||||||
|
console.log(networkError);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ApolloClient({
|
||||||
|
connectToDevTools: process.env['NODE_ENV'] === 'development',
|
||||||
|
link: from([errorLink, retryLink, splitLink]),
|
||||||
|
cache,
|
||||||
|
});
|
||||||
|
}
|
@ -18,6 +18,6 @@
|
|||||||
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" class="h-full max-h-full"></div>
|
<div id="root" class="h-full max-h-full min-h-full"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { ThemeContext } from '@vegaprotocol/react-helpers';
|
||||||
|
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||||
|
import { createClient } from './app/lib/apollo-client';
|
||||||
|
|
||||||
import App from './app/app';
|
import App from './app/app';
|
||||||
|
|
||||||
@ -8,6 +11,12 @@ const root = rootElement && createRoot(rootElement);
|
|||||||
|
|
||||||
root?.render(
|
root?.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<EnvironmentProvider>
|
||||||
|
<ThemeContext.Provider value="light">
|
||||||
|
<NetworkLoader createClient={createClient}>
|
||||||
|
<App />
|
||||||
|
</NetworkLoader>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
</EnvironmentProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -5,4 +5,6 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply h-full;
|
@apply h-full;
|
||||||
|
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
20
libs/liquidity/src/lib/MarketsLiquidity.graphql
Normal file
20
libs/liquidity/src/lib/MarketsLiquidity.graphql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
query LiquidityProvisionMarkets {
|
||||||
|
marketsConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
liquidityProvisionsConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
commitmentAmount
|
||||||
|
fee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data {
|
||||||
|
targetStake
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
libs/liquidity/src/lib/__generated__/LiquidityProvisionMarkets.ts
generated
Normal file
77
libs/liquidity/src/lib/__generated__/LiquidityProvisionMarkets.ts
generated
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: LiquidityProvisionMarkets
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets_marketsConnection_edges_node_liquidityProvisionsConnection_edges_node {
|
||||||
|
__typename: "LiquidityProvision";
|
||||||
|
/**
|
||||||
|
* Specified as a unit-less number that represents the amount of settlement asset of the market.
|
||||||
|
*/
|
||||||
|
commitmentAmount: string;
|
||||||
|
/**
|
||||||
|
* Nominated liquidity fee factor, which is an input to the calculation of liquidity fees on the market, as per setting fees and rewarding liquidity providers.
|
||||||
|
*/
|
||||||
|
fee: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets_marketsConnection_edges_node_liquidityProvisionsConnection_edges {
|
||||||
|
__typename: "LiquidityProvisionsEdge";
|
||||||
|
node: LiquidityProvisionMarkets_marketsConnection_edges_node_liquidityProvisionsConnection_edges_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets_marketsConnection_edges_node_liquidityProvisionsConnection {
|
||||||
|
__typename: "LiquidityProvisionsConnection";
|
||||||
|
edges: (LiquidityProvisionMarkets_marketsConnection_edges_node_liquidityProvisionsConnection_edges | null)[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets_marketsConnection_edges_node_data {
|
||||||
|
__typename: "MarketData";
|
||||||
|
/**
|
||||||
|
* The amount of stake targeted for this market
|
||||||
|
*/
|
||||||
|
targetStake: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets_marketsConnection_edges_node {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* Market ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The list of the liquidity provision commitments for this market
|
||||||
|
*/
|
||||||
|
liquidityProvisionsConnection: LiquidityProvisionMarkets_marketsConnection_edges_node_liquidityProvisionsConnection | null;
|
||||||
|
/**
|
||||||
|
* marketData for the given market
|
||||||
|
*/
|
||||||
|
data: LiquidityProvisionMarkets_marketsConnection_edges_node_data | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets_marketsConnection_edges {
|
||||||
|
__typename: "MarketEdge";
|
||||||
|
/**
|
||||||
|
* The market
|
||||||
|
*/
|
||||||
|
node: LiquidityProvisionMarkets_marketsConnection_edges_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets_marketsConnection {
|
||||||
|
__typename: "MarketConnection";
|
||||||
|
/**
|
||||||
|
* The markets in this connection
|
||||||
|
*/
|
||||||
|
edges: LiquidityProvisionMarkets_marketsConnection_edges[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiquidityProvisionMarkets {
|
||||||
|
/**
|
||||||
|
* One or more instruments that are trading on the Vega network
|
||||||
|
*/
|
||||||
|
marketsConnection: LiquidityProvisionMarkets_marketsConnection | null;
|
||||||
|
}
|
1
libs/liquidity/src/lib/__generated__/index.ts
generated
1
libs/liquidity/src/lib/__generated__/index.ts
generated
@ -1 +1,2 @@
|
|||||||
export * from './MarketLiquidity';
|
export * from './MarketLiquidity';
|
||||||
|
export * from './LiquidityProvisionMarkets';
|
||||||
|
60
libs/liquidity/src/lib/__generated___/MarketsLiquidity.ts
Normal file
60
libs/liquidity/src/lib/__generated___/MarketsLiquidity.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Schema as Types } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type LiquidityProvisionMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
|
||||||
|
|
||||||
|
|
||||||
|
export const LiquidityProvisionMarketsDocument = gql`
|
||||||
|
query LiquidityProvisionMarkets {
|
||||||
|
marketsConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
liquidityProvisionsConnection {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
commitmentAmount
|
||||||
|
fee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data {
|
||||||
|
targetStake
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useLiquidityProvisionMarketsQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useLiquidityProvisionMarketsQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useLiquidityProvisionMarketsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useLiquidityProvisionMarketsQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useLiquidityProvisionMarketsQuery(baseOptions?: Apollo.QueryHookOptions<LiquidityProvisionMarketsQuery, LiquidityProvisionMarketsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<LiquidityProvisionMarketsQuery, LiquidityProvisionMarketsQueryVariables>(LiquidityProvisionMarketsDocument, options);
|
||||||
|
}
|
||||||
|
export function useLiquidityProvisionMarketsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<LiquidityProvisionMarketsQuery, LiquidityProvisionMarketsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<LiquidityProvisionMarketsQuery, LiquidityProvisionMarketsQueryVariables>(LiquidityProvisionMarketsDocument, options);
|
||||||
|
}
|
||||||
|
export type LiquidityProvisionMarketsQueryHookResult = ReturnType<typeof useLiquidityProvisionMarketsQuery>;
|
||||||
|
export type LiquidityProvisionMarketsLazyQueryHookResult = ReturnType<typeof useLiquidityProvisionMarketsLazyQuery>;
|
||||||
|
export type LiquidityProvisionMarketsQueryResult = Apollo.QueryResult<LiquidityProvisionMarketsQuery, LiquidityProvisionMarketsQueryVariables>;
|
@ -1,3 +1,5 @@
|
|||||||
export * from './__generated__';
|
export * from './__generated__';
|
||||||
export * from './liquidity-data-provider';
|
export * from './liquidity-data-provider';
|
||||||
export * from './liquidity-table';
|
export * from './liquidity-table';
|
||||||
|
export * from './markets-liquidity-provider';
|
||||||
|
export * from './utils';
|
||||||
|
134
libs/liquidity/src/lib/markets-liquidity-provider.ts
Normal file
134
libs/liquidity/src/lib/markets-liquidity-provider.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Interval } from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
makeDataProvider,
|
||||||
|
makeDerivedDataProvider,
|
||||||
|
useDataProvider,
|
||||||
|
useYesterday,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
MarketCandles,
|
||||||
|
MarketWithCandles,
|
||||||
|
MarketWithData,
|
||||||
|
} from '@vegaprotocol/market-list';
|
||||||
|
|
||||||
|
import {
|
||||||
|
marketsCandlesProvider,
|
||||||
|
marketListProvider,
|
||||||
|
} from '@vegaprotocol/market-list';
|
||||||
|
|
||||||
|
import type { LiquidityProvisionMarkets } from './__generated__';
|
||||||
|
import { LiquidityProvisionMarketsDocument } from './__generated___/MarketsLiquidity';
|
||||||
|
|
||||||
|
import {
|
||||||
|
calcDayVolume,
|
||||||
|
getCandle24hAgo,
|
||||||
|
getChange,
|
||||||
|
getLiquidityForMarket,
|
||||||
|
sumLiquidityCommitted,
|
||||||
|
getFeeLevels,
|
||||||
|
getTargetStake,
|
||||||
|
} from './utils/liquidity-utils';
|
||||||
|
import type { Provider, LiquidityProvisionMarket } from './utils';
|
||||||
|
|
||||||
|
interface FeeLevels {
|
||||||
|
commitmentAmount: number;
|
||||||
|
fee: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Market = MarketWithData &
|
||||||
|
MarketWithCandles & { feeLevels: FeeLevels[]; target: string };
|
||||||
|
|
||||||
|
export interface Markets {
|
||||||
|
markets: Market[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getData = (
|
||||||
|
responseData: LiquidityProvisionMarkets
|
||||||
|
): LiquidityProvisionMarket[] | null => {
|
||||||
|
return (
|
||||||
|
responseData?.marketsConnection?.edges.map((edge) => {
|
||||||
|
return edge.node;
|
||||||
|
}) || null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addData = (
|
||||||
|
markets: (MarketWithData & MarketWithCandles)[],
|
||||||
|
marketsCandles24hAgo: MarketCandles[],
|
||||||
|
marketsLiquidity: LiquidityProvisionMarket[]
|
||||||
|
) => {
|
||||||
|
return markets.map((market) => {
|
||||||
|
const dayVolume = calcDayVolume(market.candles);
|
||||||
|
const candle24hAgo = getCandle24hAgo(market.id, marketsCandles24hAgo);
|
||||||
|
|
||||||
|
const volumeChange = getChange(market.candles || [], candle24hAgo?.close);
|
||||||
|
|
||||||
|
const liquidityProviders = getLiquidityForMarket(
|
||||||
|
market.id,
|
||||||
|
marketsLiquidity
|
||||||
|
) as Provider[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...market,
|
||||||
|
dayVolume,
|
||||||
|
volumeChange,
|
||||||
|
liquidityCommitted: sumLiquidityCommitted(liquidityProviders),
|
||||||
|
feeLevels: getFeeLevels(liquidityProviders) || [],
|
||||||
|
target: getTargetStake(market.id, marketsLiquidity),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const liquidityMarketsProvider = makeDataProvider<
|
||||||
|
LiquidityProvisionMarkets,
|
||||||
|
LiquidityProvisionMarket[],
|
||||||
|
never,
|
||||||
|
never
|
||||||
|
>({
|
||||||
|
query: LiquidityProvisionMarketsDocument,
|
||||||
|
getData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const liquidityProvisionProvider = makeDerivedDataProvider<Markets, never>(
|
||||||
|
[
|
||||||
|
marketListProvider,
|
||||||
|
(callback, client, variables) =>
|
||||||
|
marketsCandlesProvider(callback, client, {
|
||||||
|
...variables,
|
||||||
|
interval: Interval.INTERVAL_I1D,
|
||||||
|
}),
|
||||||
|
liquidityMarketsProvider,
|
||||||
|
],
|
||||||
|
(parts) => {
|
||||||
|
const data = addData(
|
||||||
|
parts[0] as (MarketWithData & MarketWithCandles)[],
|
||||||
|
parts[1] as MarketCandles[],
|
||||||
|
parts[2] as LiquidityProvisionMarket[]
|
||||||
|
);
|
||||||
|
return { markets: data };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useMarketsLiquidity = () => {
|
||||||
|
const yesterday = useYesterday();
|
||||||
|
const variables = useMemo(() => {
|
||||||
|
return {
|
||||||
|
since: new Date(yesterday).toISOString(),
|
||||||
|
interval: Interval.INTERVAL_I1H,
|
||||||
|
};
|
||||||
|
}, [yesterday]);
|
||||||
|
|
||||||
|
const { data, loading, error } = useDataProvider({
|
||||||
|
dataProvider: liquidityProvisionProvider,
|
||||||
|
variables,
|
||||||
|
noUpdate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
1
libs/liquidity/src/lib/utils/index.ts
Normal file
1
libs/liquidity/src/lib/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './liquidity-utils';
|
120
libs/liquidity/src/lib/utils/liquidity-utils.spec.tsx
Normal file
120
libs/liquidity/src/lib/utils/liquidity-utils.spec.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
formatWithAsset,
|
||||||
|
sumLiquidityCommitted,
|
||||||
|
getFeeLevels,
|
||||||
|
calcDayVolume,
|
||||||
|
getCandle24hAgo,
|
||||||
|
getChange,
|
||||||
|
EMPTY_VALUE,
|
||||||
|
} from './liquidity-utils';
|
||||||
|
|
||||||
|
const CANDLES_1 = [
|
||||||
|
{ volume: '10', open: '11', close: '11' },
|
||||||
|
{ volume: '20', open: '12', close: '12' },
|
||||||
|
{ volume: '30', open: '13', close: '13' },
|
||||||
|
];
|
||||||
|
const CANDLES_2 = [
|
||||||
|
{ volume: '30', open: '23', close: '23' },
|
||||||
|
{ volume: '20', open: '12', close: '12' },
|
||||||
|
{ volume: '10', open: '21', close: '21' },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('formatWithAsset', () => {
|
||||||
|
it('should return formatted string', () => {
|
||||||
|
const result = formatWithAsset('103926176181', {
|
||||||
|
decimals: 5,
|
||||||
|
symbol: 'tEURO',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual('1,039,261.76181 tEURO');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sumLiquidityCommitted', () => {
|
||||||
|
it('should return the total sum', () => {
|
||||||
|
const provider1 = 10;
|
||||||
|
const provider2 = 20;
|
||||||
|
const provider3 = 30;
|
||||||
|
const providers = [
|
||||||
|
{
|
||||||
|
commitmentAmount: `${provider1}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commitmentAmount: `${provider2}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commitmentAmount: `${provider3}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = sumLiquidityCommitted(providers);
|
||||||
|
expect(result).toEqual(provider1 + provider2 + provider3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFeeLevels', () => {
|
||||||
|
it('should return providers grouped by fees', () => {
|
||||||
|
const result = getFeeLevels([
|
||||||
|
{
|
||||||
|
fee: '0.2',
|
||||||
|
commitmentAmount: '10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fee: '0.1',
|
||||||
|
commitmentAmount: '10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fee: '0.1',
|
||||||
|
commitmentAmount: '20',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ fee: '0.1', commitmentAmount: 30 },
|
||||||
|
{ fee: '0.2', commitmentAmount: 10 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calcDayVolume', () => {
|
||||||
|
it('should return the volume', () => {
|
||||||
|
const candles = CANDLES_1;
|
||||||
|
const result = calcDayVolume(candles);
|
||||||
|
|
||||||
|
expect(result).toEqual('60');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCandle24hAgo', () => {
|
||||||
|
it('should return the the candle', () => {
|
||||||
|
const MARKET_ID = '123';
|
||||||
|
const CANDLES = [
|
||||||
|
{ marketId: '456', candles: CANDLES_2 },
|
||||||
|
{ marketId: MARKET_ID, candles: CANDLES_1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = getCandle24hAgo(MARKET_ID, CANDLES);
|
||||||
|
|
||||||
|
expect(result).toEqual(CANDLES_1[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getChange', () => {
|
||||||
|
it('should return the change', () => {
|
||||||
|
const lastClose = CANDLES_2[0].close;
|
||||||
|
const result = getChange(CANDLES_1, lastClose);
|
||||||
|
|
||||||
|
expect(result).toEqual('109.091%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the change if no close', () => {
|
||||||
|
const result = getChange(CANDLES_1);
|
||||||
|
|
||||||
|
expect(result).toEqual('18.182%');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty if no candles', () => {
|
||||||
|
const result = getChange([null]);
|
||||||
|
|
||||||
|
expect(result).toEqual(EMPTY_VALUE);
|
||||||
|
});
|
||||||
|
});
|
117
libs/liquidity/src/lib/utils/liquidity-utils.ts
Normal file
117
libs/liquidity/src/lib/utils/liquidity-utils.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
import type { LiquidityProvisionMarkets_marketsConnection_edges_node } from './../__generated__';
|
||||||
|
|
||||||
|
export type LiquidityProvisionMarket =
|
||||||
|
LiquidityProvisionMarkets_marketsConnection_edges_node;
|
||||||
|
|
||||||
|
export interface Provider {
|
||||||
|
commitmentAmount: string;
|
||||||
|
fee: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sumLiquidityCommitted = (
|
||||||
|
providers: Array<{ commitmentAmount: string }>
|
||||||
|
) => {
|
||||||
|
return providers
|
||||||
|
? providers.reduce((total: number, { commitmentAmount }) => {
|
||||||
|
return total + parseInt(commitmentAmount, 10);
|
||||||
|
}, 0)
|
||||||
|
: 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatWithAsset = (
|
||||||
|
value: string,
|
||||||
|
settlementAsset: {
|
||||||
|
decimals: number;
|
||||||
|
symbol: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const formattedValue = addDecimalsFormatNumber(
|
||||||
|
value,
|
||||||
|
settlementAsset.decimals
|
||||||
|
);
|
||||||
|
const symbol = settlementAsset.symbol;
|
||||||
|
return `${formattedValue} ${symbol}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Candle {
|
||||||
|
open: string;
|
||||||
|
close: string;
|
||||||
|
volume: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCandle24hAgo = (
|
||||||
|
marketId: string,
|
||||||
|
candles24hAgo: { marketId: string; candles: Candle[] | undefined }[]
|
||||||
|
) => {
|
||||||
|
return candles24hAgo.find((c) => c.marketId === marketId)?.candles?.[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EMPTY_VALUE = ' - ';
|
||||||
|
export const getChange = (candles: (Candle | null)[], lastClose?: string) => {
|
||||||
|
const firstCandle = candles.find((item) => item?.open);
|
||||||
|
if (firstCandle) {
|
||||||
|
const first = parseInt(firstCandle?.open || '-1');
|
||||||
|
const last =
|
||||||
|
typeof lastClose === 'undefined'
|
||||||
|
? candles.reduceRight((aggr, item) => {
|
||||||
|
if (aggr === -1 && item?.close) {
|
||||||
|
aggr = parseInt(item.close);
|
||||||
|
}
|
||||||
|
return aggr;
|
||||||
|
}, -1)
|
||||||
|
: parseInt(lastClose);
|
||||||
|
|
||||||
|
if (first !== -1 && last !== -1) {
|
||||||
|
return Number(((last - first) / first) * 100).toFixed(3) + '%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY_VALUE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calcDayVolume = (candles: Array<{ volume: string }> = []) => {
|
||||||
|
return candles
|
||||||
|
.reduce((acc, c) => {
|
||||||
|
return acc.plus(new BigNumber(c?.volume ?? 0));
|
||||||
|
}, new BigNumber(0))
|
||||||
|
.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFeeLevels = (providers: Provider[]) => {
|
||||||
|
const lp = providers.reduce((total: { [x: string]: number }, current) => {
|
||||||
|
const { fee, commitmentAmount } = current;
|
||||||
|
const ca = parseInt(commitmentAmount, 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...total,
|
||||||
|
[fee]: total[fee] ? total[fee] + ca : ca,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const sortedProviders = Object.keys(lp)
|
||||||
|
.sort()
|
||||||
|
.map((p) => ({ fee: p, commitmentAmount: lp[p] }));
|
||||||
|
|
||||||
|
return sortedProviders;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLiquidityForMarket = (
|
||||||
|
marketId: string,
|
||||||
|
markets: LiquidityProvisionMarket[]
|
||||||
|
) => {
|
||||||
|
const liquidity =
|
||||||
|
markets.find((m) => m.id === marketId)?.liquidityProvisionsConnection
|
||||||
|
?.edges || [];
|
||||||
|
|
||||||
|
return liquidity.map((l) => l?.node);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTargetStake = (
|
||||||
|
marketId: string,
|
||||||
|
markets: LiquidityProvisionMarket[]
|
||||||
|
) => {
|
||||||
|
return markets.find((m) => m.id === marketId)?.data?.targetStake || '0';
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user