diff --git a/apps/trading-e2e/src/integration/global.cy.ts b/apps/trading-e2e/src/integration/global.cy.ts index 0736720a9..c4c468351 100644 --- a/apps/trading-e2e/src/integration/global.cy.ts +++ b/apps/trading-e2e/src/integration/global.cy.ts @@ -10,6 +10,7 @@ describe('vega wallet', () => { beforeEach(() => { // Using portfolio page as it requires vega wallet connection cy.visit('/portfolio'); + cy.get('main[data-testid="portfolio"]').should('exist'); }); it('can connect', () => { @@ -64,8 +65,10 @@ describe('vega wallet', () => { describe('ethereum wallet', () => { beforeEach(() => { cy.mockWeb3Provider(); - // Using portfolio is it requires Ethereum wallet connection + // Using portfolio withdrawals tab is it requires Ethereum wallet connection cy.visit('/portfolio'); + cy.get('main[data-testid="portfolio"]').should('exist'); + cy.getByTestId('Withdrawals').click(); }); it('can connect', () => { @@ -73,6 +76,6 @@ describe('ethereum wallet', () => { cy.getByTestId('web3-connector-list').should('exist'); cy.getByTestId('web3-connector-MetaMask').click(); cy.getByTestId('web3-connector-list').should('not.exist'); - cy.getByTestId('portfolio-grid').should('exist'); + cy.getByTestId('tab-withdrawals').should('not.be.empty'); }); }); diff --git a/apps/trading-e2e/src/integration/portfolio-fills.cy.ts b/apps/trading-e2e/src/integration/portfolio-fills.cy.ts new file mode 100644 index 000000000..f22eeacd8 --- /dev/null +++ b/apps/trading-e2e/src/integration/portfolio-fills.cy.ts @@ -0,0 +1,120 @@ +import { aliasQuery } from '@vegaprotocol/cypress'; +import { generateFill, generateFills } from '../support/mocks/generate-fills'; +import { Side } from '@vegaprotocol/types'; +import { connectVegaWallet } from '../support/vega-wallet'; + +describe('fills', () => { + before(() => { + const fills = [ + generateFill({ + buyer: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + }), + generateFill({ + id: '1', + seller: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + aggressor: Side.Sell, + buyerFee: { + infrastructureFee: '5000', + }, + market: { + name: 'Apples Daily v3', + positionDecimalPlaces: 2, + }, + }), + generateFill({ + id: '2', + seller: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + aggressor: Side.Buy, + }), + generateFill({ + id: '3', + aggressor: Side.Sell, + market: { + name: 'ETHBTC Quarterly (30 Jun 2022)', + }, + buyer: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + }), + ]; + const result = generateFills({ + party: { + tradesPaged: { + edges: fills.map((f, i) => { + return { + __typename: 'TradeEdge', + node: f, + cursor: i.toString(), + }; + }), + }, + }, + }); + cy.mockGQL((req) => { + aliasQuery(req, 'Fills', result); + }); + cy.visit('/portfolio'); + cy.get('main[data-testid="portfolio"]').should('exist'); + }); + + it('renders fills', () => { + cy.getByTestId('Fills').click(); + cy.getByTestId('tab-fills').contains('Please connect Vega wallet'); + + connectVegaWallet(); + + cy.getByTestId('tab-fills').should('be.visible'); + + cy.getByTestId('tab-fills') + .get('[role="gridcell"][col-id="market.name"]') + .each(($marketSymbol) => { + cy.wrap($marketSymbol).invoke('text').should('not.be.empty'); + }); + cy.getByTestId('tab-fills') + .get('[role="gridcell"][col-id="size"]') + .each(($amount) => { + cy.wrap($amount).invoke('text').should('not.be.empty'); + }); + cy.getByTestId('tab-positions') + .get('[role="gridcell"][col-id="price"]') + .each(($prices) => { + cy.wrap($prices).invoke('text').should('not.be.empty'); + }); + cy.getByTestId('tab-positions') + .get('[role="gridcell"][col-id="price_1"]') + .each(($total) => { + cy.wrap($total).invoke('text').should('not.be.empty'); + }); + cy.getByTestId('tab-positions') + .get('[role="gridcell"][col-id="aggressor"]') + .each(($role) => { + cy.wrap($role) + .invoke('text') + .then((text) => { + const roles = ['Maker', 'Taker']; + expect(roles.indexOf(text.trim())).to.be.greaterThan(-1); + }); + }); + cy.getByTestId('tab-positions') + .get( + '[role="gridcell"][col-id="market.tradableInstrument.instrument.product"]' + ) + .each(($fees) => { + cy.wrap($fees).invoke('text').should('not.be.empty'); + }); + const dateTimeRegex = + /(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm; + cy.get('[col-id="createdAt"]').each(($tradeDateTime, index) => { + if (index != 0) { + //ignore header + cy.wrap($tradeDateTime).invoke('text').should('match', dateTimeRegex); + } + }); + }); +}); diff --git a/apps/trading-e2e/src/integration/portfolio.cy.ts b/apps/trading-e2e/src/integration/portfolio.cy.ts deleted file mode 100644 index 7415cac05..000000000 --- a/apps/trading-e2e/src/integration/portfolio.cy.ts +++ /dev/null @@ -1,6 +0,0 @@ -describe('portfolio', () => { - it('requires connecting', () => { - cy.visit('/portfolio'); - cy.get('main[data-testid="portfolio"]').should('exist'); - }); -}); diff --git a/apps/trading-e2e/src/support/mocks/generate-fills.ts b/apps/trading-e2e/src/support/mocks/generate-fills.ts new file mode 100644 index 000000000..fa55d9840 --- /dev/null +++ b/apps/trading-e2e/src/support/mocks/generate-fills.ts @@ -0,0 +1,134 @@ +import type { + Fills, + Fills_party_tradesPaged_edges_node, +} from '@vegaprotocol/fills'; +import { Side } from '@vegaprotocol/types'; +import merge from 'lodash/merge'; +import type { PartialDeep } from 'type-fest'; + +export const generateFills = (override?: PartialDeep): Fills => { + const fills: Fills_party_tradesPaged_edges_node[] = [ + generateFill({ + buyer: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + }), + generateFill({ + id: '1', + seller: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + aggressor: Side.Sell, + buyerFee: { + infrastructureFee: '5000', + }, + market: { + name: 'Apples Daily v3', + positionDecimalPlaces: 2, + }, + }), + generateFill({ + id: '2', + seller: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + aggressor: Side.Buy, + }), + generateFill({ + id: '3', + aggressor: Side.Sell, + market: { + name: 'ETHBTC Quarterly (30 Jun 2022)', + }, + buyer: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + }, + }), + ]; + + const defaultResult: Fills = { + party: { + id: 'buyer-id', + tradesPaged: { + __typename: 'TradeConnection', + totalCount: 1, + edges: fills.map((f) => { + return { + __typename: 'TradeEdge', + node: f, + cursor: '3', + }; + }), + pageInfo: { + __typename: 'PageInfo', + startCursor: '1', + endCursor: '2', + }, + }, + __typename: 'Party', + }, + }; + + return merge(defaultResult, override); +}; + +export const generateFill = ( + override?: PartialDeep +) => { + const defaultFill: Fills_party_tradesPaged_edges_node = { + __typename: 'Trade', + id: '0', + createdAt: new Date().toISOString(), + price: '10000000', + size: '50000', + buyOrder: 'buy-order', + sellOrder: 'sell-order', + aggressor: Side.Buy, + buyer: { + __typename: 'Party', + id: 'buyer-id', + }, + seller: { + __typename: 'Party', + id: 'seller-id', + }, + buyerFee: { + __typename: 'TradeFee', + makerFee: '100', + infrastructureFee: '100', + liquidityFee: '100', + }, + sellerFee: { + __typename: 'TradeFee', + makerFee: '200', + infrastructureFee: '200', + liquidityFee: '200', + }, + market: { + __typename: 'Market', + id: 'market-id', + name: 'UNIDAI Monthly (30 Jun 2022)', + positionDecimalPlaces: 0, + decimalPlaces: 5, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: 'instrument-id', + code: 'instrument-code', + product: { + __typename: 'Future', + settlementAsset: { + __typename: 'Asset', + id: 'asset-id', + symbol: 'SYM', + decimals: 18, + }, + }, + }, + }, + }, + }; + + return merge(defaultFill, override); +}; diff --git a/apps/trading/pages/portfolio/index.page.tsx b/apps/trading/pages/portfolio/index.page.tsx index 143ce1366..43310c627 100644 --- a/apps/trading/pages/portfolio/index.page.tsx +++ b/apps/trading/pages/portfolio/index.page.tsx @@ -5,71 +5,76 @@ import { OrderListContainer } from '@vegaprotocol/order-list'; import { AccountsContainer } from '@vegaprotocol/accounts'; import { AnchorButton, Tab, Tabs } from '@vegaprotocol/ui-toolkit'; import { WithdrawalsContainer } from './withdrawals/withdrawals-container'; +import { FillsContainer } from '@vegaprotocol/fills'; +import classNames from 'classnames'; +import type { ReactNode } from 'react'; const Portfolio = () => { - const tabClassName = 'p-[16px] pl-[316px]'; - + const wrapperClasses = classNames( + 'h-full max-h-full', + 'grid gap-4 grid-rows-[1fr_300px]', + 'bg-black-10 dark:bg-white-10', + 'text-ui' + ); + const tabContentClassName = 'h-full grid gap-4 grid-rows-[min-content_1fr]'; return ( - -
-
- -
- - -
-

- {t('Positions')} -

- -
-
- -
-

- {t('Orders')} -

- -
-
- -
-

- {t('Fills')} -

-
-
- -
-

- {t('History')} -

-
-
-
-
-
-
- - - - - - - {t('Deposit')} - - - +
+ + + +
+

+ {t('Positions')} +

+
+ +
+
+
+ +
+

+ {t('Orders')} +

+
+ +
+
+
+ +
+

+ {t('Fills')} +

+
+ +
+
+
+
+
+ + + + + + +
+
+ + {t('Deposit')} + +
+
+
+ + - -
-
-
-
+ + + + + ); }; @@ -78,3 +83,16 @@ Portfolio.getInitialProps = () => ({ }); export default Portfolio; + +interface PortfolioGridChildProps { + children: ReactNode; + className?: string; +} + +const PortfolioGridChild = ({ + children, + className, +}: PortfolioGridChildProps) => { + const gridChildClasses = classNames('bg-white dark:bg-black', className); + return
{children}
; +}; diff --git a/libs/fills/.babelrc b/libs/fills/.babelrc new file mode 100644 index 000000000..ccae900be --- /dev/null +++ b/libs/fills/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/fills/.eslintrc.json b/libs/fills/.eslintrc.json new file mode 100644 index 000000000..db820c5d0 --- /dev/null +++ b/libs/fills/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*", "__generated__"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/fills/.storybook/main.js b/libs/fills/.storybook/main.js new file mode 100644 index 000000000..9997fd7a1 --- /dev/null +++ b/libs/fills/.storybook/main.js @@ -0,0 +1,28 @@ +const rootMain = require('../../../.storybook/main'); + +module.exports = { + ...rootMain, + + core: { ...rootMain.core, builder: 'webpack5' }, + + stories: [ + ...rootMain.stories, + '../src/lib/**/*.stories.mdx', + '../src/lib/**/*.stories.@(js|jsx|ts|tsx)', + ], + addons: [ + ...rootMain.addons, + '@nrwl/react/plugins/storybook', + 'storybook-addon-themes', + ], + webpackFinal: async (config, { configType }) => { + // apply any global webpack configs that might have been specified in .storybook/main.js + if (rootMain.webpackFinal) { + config = await rootMain.webpackFinal(config, { configType }); + } + + // add your own webpack tweaks if needed + + return config; + }, +}; diff --git a/libs/fills/.storybook/preview-head.html b/libs/fills/.storybook/preview-head.html new file mode 100644 index 000000000..dd2e70030 --- /dev/null +++ b/libs/fills/.storybook/preview-head.html @@ -0,0 +1 @@ + diff --git a/libs/fills/.storybook/preview.js b/libs/fills/.storybook/preview.js new file mode 100644 index 000000000..fd45f3d42 --- /dev/null +++ b/libs/fills/.storybook/preview.js @@ -0,0 +1,51 @@ +import './styles.css'; +import { ThemeContext } from '@vegaprotocol/react-helpers'; +import { useEffect, useState } from 'react'; + +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, + backgrounds: { disable: true }, + themes: { + default: 'dark', + list: [ + { name: 'dark', class: ['dark', 'bg-black'], color: '#000' }, + { name: 'light', class: '', color: '#FFF' }, + ], + }, +}; + +export const decorators = [ + (Story, context) => { + // storybook-addon-themes doesnt seem to provide the current selected + // theme in context, we need to provid it in JS as some components + // rely on it for rendering + const [theme, setTheme] = useState(context.parameters.themes.default); + + useEffect(() => { + const observer = new MutationObserver((mutationList) => { + if (mutationList.length) { + const body = mutationList[0].target; + if (body.classList.contains('dark')) { + setTheme('dark'); + } else { + setTheme('light'); + } + } + }); + + observer.observe(document.body, { attributes: true }); + + return () => { + observer.disconnect(); + }; + }, []); + + return ( +
+ + + +
+ ); + }, +]; diff --git a/libs/fills/.storybook/styles.css b/libs/fills/.storybook/styles.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/libs/fills/.storybook/styles.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/libs/fills/.storybook/tsconfig.json b/libs/fills/.storybook/tsconfig.json new file mode 100644 index 000000000..7a1170995 --- /dev/null +++ b/libs/fills/.storybook/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "emitDecoratorMetadata": true, + "outDir": "" + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts", + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "../**/*.spec.ts", + "../**/*.spec.js", + "../**/*.spec.tsx", + "../**/*.spec.jsx" + ], + "include": ["../src/**/*", "*.js"] +} diff --git a/libs/fills/README.md b/libs/fills/README.md new file mode 100644 index 000000000..080b67b03 --- /dev/null +++ b/libs/fills/README.md @@ -0,0 +1,7 @@ +# fills + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test fills` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/fills/jest.config.js b/libs/fills/jest.config.js new file mode 100644 index 000000000..5daeb4ac3 --- /dev/null +++ b/libs/fills/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'fills', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/fills', + setupFilesAfterEnv: ['./src/setup-tests.ts'], +}; diff --git a/libs/fills/package.json b/libs/fills/package.json new file mode 100644 index 000000000..cb30ed0b7 --- /dev/null +++ b/libs/fills/package.json @@ -0,0 +1,4 @@ +{ + "name": "@vegaprotocol/fills", + "version": "0.0.1" +} diff --git a/libs/fills/postcss.config.js b/libs/fills/postcss.config.js new file mode 100644 index 000000000..cbdd9c22c --- /dev/null +++ b/libs/fills/postcss.config.js @@ -0,0 +1,10 @@ +const { join } = require('path'); + +module.exports = { + plugins: { + tailwindcss: { + config: join(__dirname, 'tailwind.config.js'), + }, + autoprefixer: {}, + }, +}; diff --git a/libs/fills/project.json b/libs/fills/project.json new file mode 100644 index 000000000..96f9c9196 --- /dev/null +++ b/libs/fills/project.json @@ -0,0 +1,74 @@ +{ + "root": "libs/fills", + "sourceRoot": "libs/fills/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/web:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/fills", + "tsConfig": "libs/fills/tsconfig.lib.json", + "project": "libs/fills/package.json", + "entryFile": "libs/fills/src/index.ts", + "external": ["react/jsx-runtime"], + "rollupConfig": "@nrwl/react/plugins/bundle-rollup", + "compiler": "babel", + "assets": [ + { + "glob": "libs/fills/README.md", + "input": ".", + "output": "." + } + ] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/fills/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/fills"], + "options": { + "jestConfig": "libs/fills/jest.config.js", + "passWithNoTests": true + } + }, + "storybook": { + "executor": "@nrwl/storybook:storybook", + "options": { + "uiFramework": "@storybook/react", + "port": 4400, + "config": { + "configFolder": "libs/fills/.storybook" + } + }, + "configurations": { + "ci": { + "quiet": true + } + } + }, + "build-storybook": { + "executor": "@nrwl/storybook:build", + "outputs": ["{options.outputPath}"], + "options": { + "uiFramework": "@storybook/react", + "outputPath": "dist/storybook/fills", + "config": { + "configFolder": "libs/fills/.storybook" + } + }, + "configurations": { + "ci": { + "quiet": true + } + } + } + } +} diff --git a/libs/fills/src/index.ts b/libs/fills/src/index.ts new file mode 100644 index 000000000..76bbc310e --- /dev/null +++ b/libs/fills/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib/fills-container'; +export * from './lib/__generated__/FillFields'; +export * from './lib/__generated__/Fills'; +export * from './lib/__generated__/FillsSub'; diff --git a/libs/fills/src/lib/__generated__/FillFields.ts b/libs/fills/src/lib/__generated__/FillFields.ts new file mode 100644 index 000000000..426e600ee --- /dev/null +++ b/libs/fills/src/lib/__generated__/FillFields.ts @@ -0,0 +1,197 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { Side } from "@vegaprotocol/types"; + +// ==================================================== +// GraphQL fragment: FillFields +// ==================================================== + +export interface FillFields_buyer { + __typename: "Party"; + /** + * Party identifier + */ + id: string; +} + +export interface FillFields_seller { + __typename: "Party"; + /** + * Party identifier + */ + id: string; +} + +export interface FillFields_buyerFee { + __typename: "TradeFee"; + /** + * The maker fee, aggressive party to the other party (the one who had an order in the book) + */ + makerFee: string; + /** + * The infrastructure fee, a fee paid to the node runner to maintain the vega network + */ + infrastructureFee: string; + /** + * The fee paid to the market makers to provide liquidity in the market + */ + liquidityFee: string; +} + +export interface FillFields_sellerFee { + __typename: "TradeFee"; + /** + * The maker fee, aggressive party to the other party (the one who had an order in the book) + */ + makerFee: string; + /** + * The infrastructure fee, a fee paid to the node runner to maintain the vega network + */ + infrastructureFee: string; + /** + * The fee paid to the market makers to provide liquidity in the market + */ + liquidityFee: string; +} + +export interface FillFields_market_tradableInstrument_instrument_product_settlementAsset { + __typename: "Asset"; + /** + * The id of the asset + */ + id: string; + /** + * The symbol of the asset (e.g: GBP) + */ + symbol: string; + /** + * The precision of the asset + */ + decimals: number; +} + +export interface FillFields_market_tradableInstrument_instrument_product { + __typename: "Future"; + /** + * The name of the asset (string) + */ + settlementAsset: FillFields_market_tradableInstrument_instrument_product_settlementAsset; +} + +export interface FillFields_market_tradableInstrument_instrument { + __typename: "Instrument"; + /** + * Uniquely identify an instrument across all instruments available on Vega (string) + */ + id: string; + /** + * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) + */ + code: string; + /** + * A reference to or instance of a fully specified product, including all required product parameters for that product (Product union) + */ + product: FillFields_market_tradableInstrument_instrument_product; +} + +export interface FillFields_market_tradableInstrument { + __typename: "TradableInstrument"; + /** + * An instance of or reference to a fully specified instrument. + */ + instrument: FillFields_market_tradableInstrument_instrument; +} + +export interface FillFields_market { + __typename: "Market"; + /** + * Market ID + */ + id: string; + /** + * Market full name + */ + name: string; + /** + * decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct + * number denominated in the currency of the Market. (uint64) + * + * Examples: + * Currency Balance decimalPlaces Real Balance + * GBP 100 0 GBP 100 + * GBP 100 2 GBP 1.00 + * GBP 100 4 GBP 0.01 + * GBP 1 4 GBP 0.0001 ( 0.01p ) + * + * GBX (pence) 100 0 GBP 1.00 (100p ) + * GBX (pence) 100 2 GBP 0.01 ( 1p ) + * GBX (pence) 100 4 GBP 0.0001 ( 0.01p ) + * GBX (pence) 1 4 GBP 0.000001 ( 0.0001p) + */ + decimalPlaces: number; + /** + * positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64). + * i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes. + * 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market. + */ + positionDecimalPlaces: number; + /** + * An instance of or reference to a tradable instrument. + */ + tradableInstrument: FillFields_market_tradableInstrument; +} + +export interface FillFields { + __typename: "Trade"; + /** + * The hash of the trade data + */ + id: string; + /** + * RFC3339Nano time for when the trade occurred + */ + createdAt: string; + /** + * The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64) + */ + price: string; + /** + * The number of contracts trades, will always be <= the remaining size of both orders immediately before the trade (uint64) + */ + size: string; + /** + * The order that bought + */ + buyOrder: string; + /** + * The order that sold + */ + sellOrder: string; + /** + * The aggressor indicates whether this trade was related to a BUY or SELL + */ + aggressor: Side; + /** + * The party that bought + */ + buyer: FillFields_buyer; + /** + * The party that sold + */ + seller: FillFields_seller; + /** + * The fee paid by the buyer side of the trade + */ + buyerFee: FillFields_buyerFee; + /** + * The fee paid by the seller side of the trade + */ + sellerFee: FillFields_sellerFee; + /** + * The market the trade occurred on + */ + market: FillFields_market; +} diff --git a/libs/fills/src/lib/__generated__/Fills.ts b/libs/fills/src/lib/__generated__/Fills.ts new file mode 100644 index 000000000..623edabeb --- /dev/null +++ b/libs/fills/src/lib/__generated__/Fills.ts @@ -0,0 +1,247 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { Pagination, Side } from "@vegaprotocol/types"; + +// ==================================================== +// GraphQL query operation: Fills +// ==================================================== + +export interface Fills_party_tradesPaged_edges_node_buyer { + __typename: "Party"; + /** + * Party identifier + */ + id: string; +} + +export interface Fills_party_tradesPaged_edges_node_seller { + __typename: "Party"; + /** + * Party identifier + */ + id: string; +} + +export interface Fills_party_tradesPaged_edges_node_buyerFee { + __typename: "TradeFee"; + /** + * The maker fee, aggressive party to the other party (the one who had an order in the book) + */ + makerFee: string; + /** + * The infrastructure fee, a fee paid to the node runner to maintain the vega network + */ + infrastructureFee: string; + /** + * The fee paid to the market makers to provide liquidity in the market + */ + liquidityFee: string; +} + +export interface Fills_party_tradesPaged_edges_node_sellerFee { + __typename: "TradeFee"; + /** + * The maker fee, aggressive party to the other party (the one who had an order in the book) + */ + makerFee: string; + /** + * The infrastructure fee, a fee paid to the node runner to maintain the vega network + */ + infrastructureFee: string; + /** + * The fee paid to the market makers to provide liquidity in the market + */ + liquidityFee: string; +} + +export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product_settlementAsset { + __typename: "Asset"; + /** + * The id of the asset + */ + id: string; + /** + * The symbol of the asset (e.g: GBP) + */ + symbol: string; + /** + * The precision of the asset + */ + decimals: number; +} + +export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product { + __typename: "Future"; + /** + * The name of the asset (string) + */ + settlementAsset: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product_settlementAsset; +} + +export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument { + __typename: "Instrument"; + /** + * Uniquely identify an instrument across all instruments available on Vega (string) + */ + id: string; + /** + * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) + */ + code: string; + /** + * A reference to or instance of a fully specified product, including all required product parameters for that product (Product union) + */ + product: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument_product; +} + +export interface Fills_party_tradesPaged_edges_node_market_tradableInstrument { + __typename: "TradableInstrument"; + /** + * An instance of or reference to a fully specified instrument. + */ + instrument: Fills_party_tradesPaged_edges_node_market_tradableInstrument_instrument; +} + +export interface Fills_party_tradesPaged_edges_node_market { + __typename: "Market"; + /** + * Market ID + */ + id: string; + /** + * Market full name + */ + name: string; + /** + * decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct + * number denominated in the currency of the Market. (uint64) + * + * Examples: + * Currency Balance decimalPlaces Real Balance + * GBP 100 0 GBP 100 + * GBP 100 2 GBP 1.00 + * GBP 100 4 GBP 0.01 + * GBP 1 4 GBP 0.0001 ( 0.01p ) + * + * GBX (pence) 100 0 GBP 1.00 (100p ) + * GBX (pence) 100 2 GBP 0.01 ( 1p ) + * GBX (pence) 100 4 GBP 0.0001 ( 0.01p ) + * GBX (pence) 1 4 GBP 0.000001 ( 0.0001p) + */ + decimalPlaces: number; + /** + * positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64). + * i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes. + * 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market. + */ + positionDecimalPlaces: number; + /** + * An instance of or reference to a tradable instrument. + */ + tradableInstrument: Fills_party_tradesPaged_edges_node_market_tradableInstrument; +} + +export interface Fills_party_tradesPaged_edges_node { + __typename: "Trade"; + /** + * The hash of the trade data + */ + id: string; + /** + * RFC3339Nano time for when the trade occurred + */ + createdAt: string; + /** + * The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64) + */ + price: string; + /** + * The number of contracts trades, will always be <= the remaining size of both orders immediately before the trade (uint64) + */ + size: string; + /** + * The order that bought + */ + buyOrder: string; + /** + * The order that sold + */ + sellOrder: string; + /** + * The aggressor indicates whether this trade was related to a BUY or SELL + */ + aggressor: Side; + /** + * The party that bought + */ + buyer: Fills_party_tradesPaged_edges_node_buyer; + /** + * The party that sold + */ + seller: Fills_party_tradesPaged_edges_node_seller; + /** + * The fee paid by the buyer side of the trade + */ + buyerFee: Fills_party_tradesPaged_edges_node_buyerFee; + /** + * The fee paid by the seller side of the trade + */ + sellerFee: Fills_party_tradesPaged_edges_node_sellerFee; + /** + * The market the trade occurred on + */ + market: Fills_party_tradesPaged_edges_node_market; +} + +export interface Fills_party_tradesPaged_edges { + __typename: "TradeEdge"; + node: Fills_party_tradesPaged_edges_node; + cursor: string; +} + +export interface Fills_party_tradesPaged_pageInfo { + __typename: "PageInfo"; + startCursor: string; + endCursor: string; +} + +export interface Fills_party_tradesPaged { + __typename: "TradeConnection"; + /** + * The total number of trades in this connection + */ + totalCount: number; + /** + * The trade in this connection + */ + edges: Fills_party_tradesPaged_edges[]; + /** + * The pagination information + */ + pageInfo: Fills_party_tradesPaged_pageInfo; +} + +export interface Fills_party { + __typename: "Party"; + /** + * Party identifier + */ + id: string; + tradesPaged: Fills_party_tradesPaged; +} + +export interface Fills { + /** + * An entity that is trading on the VEGA network + */ + party: Fills_party | null; +} + +export interface FillsVariables { + partyId: string; + marketId?: string | null; + pagination?: Pagination | null; +} diff --git a/libs/fills/src/lib/__generated__/FillsSub.ts b/libs/fills/src/lib/__generated__/FillsSub.ts new file mode 100644 index 000000000..c1b056cf0 --- /dev/null +++ b/libs/fills/src/lib/__generated__/FillsSub.ts @@ -0,0 +1,208 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { Side } from "@vegaprotocol/types"; + +// ==================================================== +// GraphQL subscription operation: FillsSub +// ==================================================== + +export interface FillsSub_trades_buyer { + __typename: "Party"; + /** + * Party identifier + */ + id: string; +} + +export interface FillsSub_trades_seller { + __typename: "Party"; + /** + * Party identifier + */ + id: string; +} + +export interface FillsSub_trades_buyerFee { + __typename: "TradeFee"; + /** + * The maker fee, aggressive party to the other party (the one who had an order in the book) + */ + makerFee: string; + /** + * The infrastructure fee, a fee paid to the node runner to maintain the vega network + */ + infrastructureFee: string; + /** + * The fee paid to the market makers to provide liquidity in the market + */ + liquidityFee: string; +} + +export interface FillsSub_trades_sellerFee { + __typename: "TradeFee"; + /** + * The maker fee, aggressive party to the other party (the one who had an order in the book) + */ + makerFee: string; + /** + * The infrastructure fee, a fee paid to the node runner to maintain the vega network + */ + infrastructureFee: string; + /** + * The fee paid to the market makers to provide liquidity in the market + */ + liquidityFee: string; +} + +export interface FillsSub_trades_market_tradableInstrument_instrument_product_settlementAsset { + __typename: "Asset"; + /** + * The id of the asset + */ + id: string; + /** + * The symbol of the asset (e.g: GBP) + */ + symbol: string; + /** + * The precision of the asset + */ + decimals: number; +} + +export interface FillsSub_trades_market_tradableInstrument_instrument_product { + __typename: "Future"; + /** + * The name of the asset (string) + */ + settlementAsset: FillsSub_trades_market_tradableInstrument_instrument_product_settlementAsset; +} + +export interface FillsSub_trades_market_tradableInstrument_instrument { + __typename: "Instrument"; + /** + * Uniquely identify an instrument across all instruments available on Vega (string) + */ + id: string; + /** + * A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string) + */ + code: string; + /** + * A reference to or instance of a fully specified product, including all required product parameters for that product (Product union) + */ + product: FillsSub_trades_market_tradableInstrument_instrument_product; +} + +export interface FillsSub_trades_market_tradableInstrument { + __typename: "TradableInstrument"; + /** + * An instance of or reference to a fully specified instrument. + */ + instrument: FillsSub_trades_market_tradableInstrument_instrument; +} + +export interface FillsSub_trades_market { + __typename: "Market"; + /** + * Market ID + */ + id: string; + /** + * Market full name + */ + name: string; + /** + * decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct + * number denominated in the currency of the Market. (uint64) + * + * Examples: + * Currency Balance decimalPlaces Real Balance + * GBP 100 0 GBP 100 + * GBP 100 2 GBP 1.00 + * GBP 100 4 GBP 0.01 + * GBP 1 4 GBP 0.0001 ( 0.01p ) + * + * GBX (pence) 100 0 GBP 1.00 (100p ) + * GBX (pence) 100 2 GBP 0.01 ( 1p ) + * GBX (pence) 100 4 GBP 0.0001 ( 0.01p ) + * GBX (pence) 1 4 GBP 0.000001 ( 0.0001p) + */ + decimalPlaces: number; + /** + * positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64). + * i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes. + * 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market. + */ + positionDecimalPlaces: number; + /** + * An instance of or reference to a tradable instrument. + */ + tradableInstrument: FillsSub_trades_market_tradableInstrument; +} + +export interface FillsSub_trades { + __typename: "Trade"; + /** + * The hash of the trade data + */ + id: string; + /** + * RFC3339Nano time for when the trade occurred + */ + createdAt: string; + /** + * The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64) + */ + price: string; + /** + * The number of contracts trades, will always be <= the remaining size of both orders immediately before the trade (uint64) + */ + size: string; + /** + * The order that bought + */ + buyOrder: string; + /** + * The order that sold + */ + sellOrder: string; + /** + * The aggressor indicates whether this trade was related to a BUY or SELL + */ + aggressor: Side; + /** + * The party that bought + */ + buyer: FillsSub_trades_buyer; + /** + * The party that sold + */ + seller: FillsSub_trades_seller; + /** + * The fee paid by the buyer side of the trade + */ + buyerFee: FillsSub_trades_buyerFee; + /** + * The fee paid by the seller side of the trade + */ + sellerFee: FillsSub_trades_sellerFee; + /** + * The market the trade occurred on + */ + market: FillsSub_trades_market; +} + +export interface FillsSub { + /** + * Subscribe to the trades updates + */ + trades: FillsSub_trades[] | null; +} + +export interface FillsSubVariables { + partyId: string; +} diff --git a/libs/fills/src/lib/fills-container.tsx b/libs/fills/src/lib/fills-container.tsx new file mode 100644 index 000000000..f5def05a1 --- /dev/null +++ b/libs/fills/src/lib/fills-container.tsx @@ -0,0 +1,18 @@ +import { t } from '@vegaprotocol/react-helpers'; +import { Splash } from '@vegaprotocol/ui-toolkit'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { FillsManager } from './fills-manager'; + +export const FillsContainer = () => { + const { keypair } = useVegaWallet(); + + if (!keypair) { + return ( + +

{t('Please connect Vega wallet')}

+
+ ); + } + + return ; +}; diff --git a/libs/fills/src/lib/fills-data-provider.ts b/libs/fills/src/lib/fills-data-provider.ts new file mode 100644 index 000000000..d22993790 --- /dev/null +++ b/libs/fills/src/lib/fills-data-provider.ts @@ -0,0 +1,117 @@ +import { gql } from '@apollo/client'; +import { makeDataProvider } from '@vegaprotocol/react-helpers'; +import produce from 'immer'; +import type { FillFields } from './__generated__/FillFields'; +import type { + Fills, + Fills_party_tradesPaged_edges_node, +} from './__generated__/Fills'; +import type { FillsSub } from './__generated__/FillsSub'; + +const FILL_FRAGMENT = gql` + fragment FillFields on Trade { + id + createdAt + price + size + buyOrder + sellOrder + aggressor + buyer { + id + } + seller { + id + } + buyerFee { + makerFee + infrastructureFee + liquidityFee + } + sellerFee { + makerFee + infrastructureFee + liquidityFee + } + market { + id + name + decimalPlaces + positionDecimalPlaces + tradableInstrument { + instrument { + id + code + product { + ... on Future { + settlementAsset { + id + symbol + decimals + } + } + } + } + } + } + } +`; + +export const FILLS_QUERY = gql` + ${FILL_FRAGMENT} + query Fills($partyId: ID!, $marketId: ID, $pagination: Pagination) { + party(id: $partyId) { + id + tradesPaged(marketId: $marketId, pagination: $pagination) { + totalCount + edges { + node { + ...FillFields + } + cursor + } + pageInfo { + startCursor + endCursor + } + } + } + } +`; + +export const FILLS_SUB = gql` + ${FILL_FRAGMENT} + subscription FillsSub($partyId: ID!) { + trades(partyId: $partyId) { + ...FillFields + } + } +`; + +const update = (data: FillFields[], delta: FillFields[]) => { + // Add or update incoming trades + return produce(data, (draft) => { + delta.forEach((trade) => { + const index = draft.findIndex((t) => t.id === trade.id); + if (index === -1) { + draft.unshift(trade); + } else { + draft[index] = trade; + } + }); + }); +}; + +const getData = ( + responseData: Fills +): Fills_party_tradesPaged_edges_node[] | null => + responseData.party?.tradesPaged.edges.map((e) => e.node) || null; +const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || []; + +export const fillsDataProvider = makeDataProvider( + FILLS_QUERY, + FILLS_SUB, + update, + getData, + getDelta +); diff --git a/libs/fills/src/lib/fills-manager.tsx b/libs/fills/src/lib/fills-manager.tsx new file mode 100644 index 000000000..0b1727e65 --- /dev/null +++ b/libs/fills/src/lib/fills-manager.tsx @@ -0,0 +1,80 @@ +import type { AgGridReact } from 'ag-grid-react'; +import { useCallback, useMemo, useRef } from 'react'; +import { FillsTable } from './fills-table'; +import { fillsDataProvider } from './fills-data-provider'; +import { useDataProvider } from '@vegaprotocol/react-helpers'; +import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; +import type { FillsVariables } from './__generated__/Fills'; +import type { FillFields } from './__generated__/FillFields'; +import type { FillsSub_trades } from './__generated__/FillsSub'; +import isEqual from 'lodash/isEqual'; + +interface FillsManagerProps { + partyId: string; +} + +export const FillsManager = ({ partyId }: FillsManagerProps) => { + const gridRef = useRef(null); + const variables = useMemo( + () => ({ + partyId, + pagination: { + last: 300, + }, + }), + [partyId] + ); + const update = useCallback((delta: FillsSub_trades[]) => { + if (!gridRef.current) { + return false; + } + const updateRows: FillFields[] = []; + const add: FillFields[] = []; + + delta.forEach((d) => { + if (!gridRef.current?.api) { + return; + } + + const rowNode = gridRef.current.api.getRowNode(d.id); + + if (rowNode) { + if (!isEqual(d, rowNode.data)) { + updateRows.push(d); + } + } else { + add.push(d); + } + }); + + if (updateRows.length || add.length) { + gridRef.current.api.applyTransactionAsync({ + update: updateRows, + add, + addIndex: 0, + }); + } + + return true; + }, []); + + const { data, loading, error } = useDataProvider( + fillsDataProvider, + update, + variables + ); + + const fills = useMemo(() => { + if (!data?.length) { + return []; + } + + return data; + }, [data]); + + return ( + + + + ); +}; diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx new file mode 100644 index 000000000..df98acf11 --- /dev/null +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -0,0 +1,177 @@ +import { render, act, screen, waitFor } from '@testing-library/react'; +import { getDateTimeFormat } from '@vegaprotocol/react-helpers'; +import { Side } from '@vegaprotocol/types'; +import type { PartialDeep } from 'type-fest'; + +import { FillsTable } from './fills-table'; +import { generateFill } from './test-helpers'; +import type { FillFields } from './__generated__/FillFields'; + +describe('FillsTable', () => { + let defaultFill: PartialDeep; + + beforeEach(() => { + defaultFill = { + price: '100', + size: '300000', + market: { + name: 'test market', + decimalPlaces: 2, + positionDecimalPlaces: 5, + tradableInstrument: { + instrument: { + product: { + settlementAsset: { + decimals: 2, + symbol: 'BTC', + }, + }, + }, + }, + }, + createdAt: new Date('2022-02-02T14:00:00').toISOString(), + }; + }); + + it('correct columns are rendered', async () => { + await act(async () => { + render(); + }); + + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(7); + expect(headers.map((h) => h.textContent?.trim())).toEqual([ + 'Market', + 'Amount', + 'Value', + 'Filled value', + 'Role', + 'Fee', + 'Date', + ]); + }); + + it('formats cells correctly for buyer fill', async () => { + const partyId = 'party-id'; + const buyerFill = generateFill({ + ...defaultFill, + buyer: { + id: partyId, + }, + aggressor: Side.Sell, + buyerFee: { + makerFee: '2', + infrastructureFee: '2', + liquidityFee: '2', + }, + }); + + const { container } = render( + + ); + + // Check grid has been rendered + await waitFor(() => { + expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument(); + }); + + const cells = screen.getAllByRole('gridcell'); + const expectedValues = [ + buyerFill.market.name, + '+3.00000', + '1.00 BTC', + '3.00 BTC', + 'Maker', + '0.06 BTC', + getDateTimeFormat().format(new Date(buyerFill.createdAt)), + ]; + cells.forEach((cell, i) => { + expect(cell).toHaveTextContent(expectedValues[i]); + }); + + const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size'); + expect(amountCell).toHaveClass('text-vega-green'); + }); + + it('formats cells correctly for seller fill', async () => { + const partyId = 'party-id'; + const buyerFill = generateFill({ + ...defaultFill, + seller: { + id: partyId, + }, + aggressor: Side.Sell, + sellerFee: { + makerFee: '1', + infrastructureFee: '1', + liquidityFee: '1', + }, + }); + + const { container } = render( + + ); + + // Check grid has been rendered + await waitFor(() => { + expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument(); + }); + + const cells = screen.getAllByRole('gridcell'); + const expectedValues = [ + buyerFill.market.name, + '-3.00000', + '1.00 BTC', + '3.00 BTC', + 'Taker', + '0.03 BTC', + getDateTimeFormat().format(new Date(buyerFill.createdAt)), + ]; + cells.forEach((cell, i) => { + expect(cell).toHaveTextContent(expectedValues[i]); + }); + + const amountCell = cells.find((c) => c.getAttribute('col-id') === 'size'); + expect(amountCell).toHaveClass('text-vega-red'); + }); + + it('renders correct maker or taker role', async () => { + const partyId = 'party-id'; + const takerFill = generateFill({ + seller: { + id: partyId, + }, + aggressor: Side.Sell, + }); + + const { container, rerender } = render( + + ); + + // Check grid has been rendered + await waitFor(() => { + expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument(); + }); + + expect( + screen + .getAllByRole('gridcell') + .find((c) => c.getAttribute('col-id') === 'aggressor') + ).toHaveTextContent('Taker'); + + const makerFill = generateFill({ + seller: { + id: partyId, + }, + aggressor: Side.Buy, + }); + + rerender(); + + expect( + screen + .getAllByRole('gridcell') + .find((c) => c.getAttribute('col-id') === 'aggressor') + ).toHaveTextContent('Maker'); + }); +}); diff --git a/libs/fills/src/lib/fills-table.stories.tsx b/libs/fills/src/lib/fills-table.stories.tsx new file mode 100644 index 000000000..d24a8b69c --- /dev/null +++ b/libs/fills/src/lib/fills-table.stories.tsx @@ -0,0 +1,18 @@ +import type { Story, Meta } from '@storybook/react'; +import type { FillsTableProps } from './fills-table'; +import { FillsTable } from './fills-table'; +import { generateFills } from './test-helpers'; + +export default { + component: FillsTable, + title: 'FillsTable', +} as Meta; + +const Template: Story = (args) => ; + +export const Default = Template.bind({}); +const fills = generateFills(); +Default.args = { + partyId: 'party-id', + fills: fills.party?.tradesPaged.edges.map((e) => e.node), +}; diff --git a/libs/fills/src/lib/fills-table.tsx b/libs/fills/src/lib/fills-table.tsx new file mode 100644 index 000000000..c8199e1ee --- /dev/null +++ b/libs/fills/src/lib/fills-table.tsx @@ -0,0 +1,160 @@ +import type { AgGridReact } from 'ag-grid-react'; +import { + addDecimal, + addDecimalsFormatNumber, + formatNumber, + getDateTimeFormat, + t, +} from '@vegaprotocol/react-helpers'; +import { AgGridColumn } from 'ag-grid-react'; +import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; +import { forwardRef } from 'react'; +import type { FillFields } from './__generated__/FillFields'; +import type { ValueFormatterParams } from 'ag-grid-community'; +import BigNumber from 'bignumber.js'; +import { Side } from '@vegaprotocol/types'; + +export interface FillsTableProps { + partyId: string; + fills: FillFields[]; +} + +export const FillsTable = forwardRef( + ({ partyId, fills }, ref) => { + return ( + data.id} + > + + { + let className = ''; + if (data.buyer.id === partyId) { + className = 'text-vega-green'; + } else if (data.seller.id) { + className = 'text-vega-red'; + } + return className; + }} + valueFormatter={formatSize(partyId)} + /> + + + + + { + return getDateTimeFormat().format(new Date(value)); + }} + /> + + ); + } +); + +const formatPrice = ({ value, data }: ValueFormatterParams) => { + const asset = + data.market.tradableInstrument.instrument.product.settlementAsset.symbol; + const valueFormatted = addDecimalsFormatNumber( + value, + data.market.decimalPlaces + ); + return `${valueFormatted} ${asset}`; +}; + +const formatSize = (partyId: string) => { + return ({ value, data }: ValueFormatterParams) => { + let prefix; + if (data.buyer.id === partyId) { + prefix = '+'; + } else if (data.seller.id) { + prefix = '-'; + } + + const size = addDecimalsFormatNumber( + value, + data.market.positionDecimalPlaces + ); + return `${prefix}${size}`; + }; +}; + +const formatTotal = ({ value, data }: ValueFormatterParams) => { + const asset = + data.market.tradableInstrument.instrument.product.settlementAsset.symbol; + const size = new BigNumber( + addDecimal(data.size, data.market.positionDecimalPlaces) + ); + const price = new BigNumber(addDecimal(value, data.market.decimalPlaces)); + + const total = size.times(price).toString(); + const valueFormatted = formatNumber(total, data.market.decimalPlaces); + return `${valueFormatted} ${asset}`; +}; + +const formatRole = (partyId: string) => { + return ({ value, data }: ValueFormatterParams) => { + const taker = t('Taker'); + const maker = t('Maker'); + if (data.buyer.id === partyId) { + if (value === Side.Buy) { + return taker; + } else { + return maker; + } + } else if (data.seller.id === partyId) { + if (value === Side.Sell) { + return taker; + } else { + return maker; + } + } else { + return '-'; + } + }; +}; + +const formatFee = (partyId: string) => { + return ({ value, data }: ValueFormatterParams) => { + const asset = value.settlementAsset; + let feesObj; + if (data.buyer.id === partyId) { + feesObj = data.buyerFee; + } else if (data.seller.id === partyId) { + feesObj = data.sellerFee; + } else { + return '-'; + } + + const fee = new BigNumber(feesObj.makerFee) + .plus(feesObj.infrastructureFee) + .plus(feesObj.liquidityFee); + const totalFees = addDecimalsFormatNumber(fee.toString(), asset.decimals); + return `${totalFees} ${asset.symbol}`; + }; +}; diff --git a/libs/fills/src/lib/test-helpers.ts b/libs/fills/src/lib/test-helpers.ts new file mode 100644 index 000000000..19c3b3506 --- /dev/null +++ b/libs/fills/src/lib/test-helpers.ts @@ -0,0 +1,134 @@ +import { Side } from '@vegaprotocol/types'; +import merge from 'lodash/merge'; +import type { PartialDeep } from 'type-fest'; +import type { + Fills, + Fills_party_tradesPaged_edges_node, +} from './__generated__/Fills'; + +export const generateFills = (override?: PartialDeep): Fills => { + const fills: Fills_party_tradesPaged_edges_node[] = [ + generateFill({ + buyer: { + id: 'party-id', + }, + }), + generateFill({ + id: '1', + seller: { + id: 'party-id', + }, + aggressor: Side.Sell, + buyerFee: { + infrastructureFee: '5000', + }, + market: { + name: 'Apples Daily v3', + positionDecimalPlaces: 2, + }, + }), + generateFill({ + id: '2', + seller: { + id: 'party-id', + }, + aggressor: Side.Buy, + }), + generateFill({ + id: '3', + aggressor: Side.Sell, + market: { + name: 'ETHBTC Quarterly (30 Jun 2022)', + }, + buyer: { + id: 'party-id', + }, + }), + ]; + + const defaultResult: Fills = { + party: { + id: 'buyer-id', + tradesPaged: { + __typename: 'TradeConnection', + totalCount: 1, + edges: fills.map((f) => { + return { + __typename: 'TradeEdge', + node: f, + cursor: '3', + }; + }), + pageInfo: { + __typename: 'PageInfo', + startCursor: '1', + endCursor: '2', + }, + }, + __typename: 'Party', + }, + }; + + return merge(defaultResult, override); +}; + +export const generateFill = ( + override?: PartialDeep +) => { + const defaultFill: Fills_party_tradesPaged_edges_node = { + __typename: 'Trade', + id: '0', + createdAt: new Date().toISOString(), + price: '10000000', + size: '50000', + buyOrder: 'buy-order', + sellOrder: 'sell-order', + aggressor: Side.Buy, + buyer: { + __typename: 'Party', + id: 'buyer-id', + }, + seller: { + __typename: 'Party', + id: 'seller-id', + }, + buyerFee: { + __typename: 'TradeFee', + makerFee: '100', + infrastructureFee: '100', + liquidityFee: '100', + }, + sellerFee: { + __typename: 'TradeFee', + makerFee: '200', + infrastructureFee: '200', + liquidityFee: '200', + }, + market: { + __typename: 'Market', + id: 'market-id', + name: 'UNIDAI Monthly (30 Jun 2022)', + positionDecimalPlaces: 0, + decimalPlaces: 5, + tradableInstrument: { + __typename: 'TradableInstrument', + instrument: { + __typename: 'Instrument', + id: 'instrument-id', + code: 'instrument-code', + product: { + __typename: 'Future', + settlementAsset: { + __typename: 'Asset', + id: 'asset-id', + symbol: 'SYM', + decimals: 18, + }, + }, + }, + }, + }, + }; + + return merge(defaultFill, override); +}; diff --git a/libs/fills/src/setup-tests.ts b/libs/fills/src/setup-tests.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/libs/fills/src/setup-tests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/libs/fills/tailwind.config.js b/libs/fills/tailwind.config.js new file mode 100644 index 000000000..1deb8143d --- /dev/null +++ b/libs/fills/tailwind.config.js @@ -0,0 +1,17 @@ +const { join } = require('path'); +const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind'); +const theme = require('../tailwindcss-config/src/theme'); +const vegaCustomClasses = require('../tailwindcss-config/src/vega-custom-classes'); + +module.exports = { + content: [ + join(__dirname, 'src/**/*.{ts,tsx,html,mdx}'), + join(__dirname, '.storybook/preview.js'), + ...createGlobPatternsForDependencies(__dirname), + ], + darkMode: 'class', + theme: { + extend: theme, + }, + plugins: [vegaCustomClasses], +}; diff --git a/libs/fills/tsconfig.json b/libs/fills/tsconfig.json new file mode 100644 index 000000000..9fff9cc2d --- /dev/null +++ b/libs/fills/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/libs/fills/tsconfig.lib.json b/libs/fills/tsconfig.lib.json new file mode 100644 index 000000000..ad9c3d024 --- /dev/null +++ b/libs/fills/tsconfig.lib.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.stories.ts", + "**/*.stories.js", + "**/*.stories.jsx", + "**/*.stories.tsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/fills/tsconfig.spec.json b/libs/fills/tsconfig.spec.json new file mode 100644 index 000000000..86a9fa994 --- /dev/null +++ b/libs/fills/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/types/src/__generated__/globalTypes.ts b/libs/types/src/__generated__/globalTypes.ts index 4e275a805..ba6288849 100644 --- a/libs/types/src/__generated__/globalTypes.ts +++ b/libs/types/src/__generated__/globalTypes.ts @@ -288,6 +288,16 @@ export enum WithdrawalStatus { Rejected = "Rejected", } +/** + * Pagination constructs to support cursor based pagination in the API + */ +export interface Pagination { + first?: number | null; + after?: string | null; + last?: number | null; + before?: string | null; +} + //============================================================== // END Enums and Input Objects //============================================================== diff --git a/tsconfig.base.json b/tsconfig.base.json index 9c8bdb87a..c3bdf503e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -22,6 +22,7 @@ "@vegaprotocol/deal-ticket": ["libs/deal-ticket/src/index.ts"], "@vegaprotocol/deposits": ["libs/deposits/src/index.ts"], "@vegaprotocol/environment": ["libs/environment/src/index.ts"], + "@vegaprotocol/fills": ["libs/fills/src/index.ts"], "@vegaprotocol/market-depth": ["libs/market-depth/src/index.ts"], "@vegaprotocol/market-list": ["libs/market-list/src/index.ts"], "@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"], diff --git a/workspace.json b/workspace.json index 5d672ae34..38526f4b7 100644 --- a/workspace.json +++ b/workspace.json @@ -9,6 +9,7 @@ "environment": "libs/environment", "explorer": "apps/explorer", "explorer-e2e": "apps/explorer-e2e", + "fills": "libs/fills", "market-depth": "libs/market-depth", "market-list": "libs/market-list", "network-stats": "libs/network-stats",