Merge branch 'master' of github.com:vegaprotocol/frontend-monorepo

This commit is contained in:
madalinaraicu
2022-08-02 20:35:43 +01:00
141 changed files with 3699 additions and 988 deletions
@@ -1,4 +1,4 @@
name: Capsule tests -- nightly
name: Capsule tests -- night run
# This workflow runs the frontend tests against latest develop of the core to preempt breaking changes
+1
View File
@@ -12,6 +12,7 @@ NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=0
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
+1
View File
@@ -11,6 +11,7 @@ NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
+1
View File
@@ -11,6 +11,7 @@ NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
+1
View File
@@ -11,6 +11,7 @@ NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
+1
View File
@@ -11,6 +11,7 @@ NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
+1
View File
@@ -23,4 +23,5 @@ NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=1
+1
View File
@@ -13,6 +13,7 @@ NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
+1
View File
@@ -58,6 +58,7 @@ There are a few different configuration options offered for this app:
| `NX_EXPLORER_GENESIS` | Enable the genesis page for the explorer |
| `NX_EXPLORER_GOVERNANCE` | Enable the governance page for the explorer |
| `NX_EXPLORER_MARKETS` | Enable the markets page for the explorer |
| `NX_EXPLORER_ORACLES` | Enable the oracles page for the explorer |
| `NX_EXPLORER_TXS_LIST` | Enable the transactions list page for the explorer |
| `NX_EXPLORER_NETWORK_PARAMETERS` | Enable the network parameters page for the explorer |
| `NX_EXPLORER_PARTIES` | Enable the parties page for the explorer |
@@ -2,7 +2,9 @@ import React from 'react';
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
import { Routes } from '../../routes/route-names';
import { Link } from 'react-router-dom';
import { SecondsAgo } from '../seconds-ago';
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { TimeAgo } from '../time-ago';
import { TableWithTbody, TableRow, TableCell } from '../table';
import { t } from '@vegaprotocol/react-helpers';
@@ -53,7 +55,16 @@ export const BlockData = ({ block, className }: BlockProps) => {
className="text-center pr-28 text-neutral-300 w-[170px]"
aria-label={t('Block genesis')}
>
<SecondsAgo date={block.header?.time} />
<Tooltip
description={getDateTimeFormat().format(
new Date(block.header.time)
)}
>
{/*For some reason we get forwardRef errors if we pass in the TimeAgo component directly*/}
<span>
<TimeAgo date={block.header.time} />
</span>
</Tooltip>
</TableCell>
</TableRow>
</TableWithTbody>
@@ -29,6 +29,7 @@ export const JumpToBlock = () => {
inputType="number"
inputName="blockNumber"
submitHandler={handleSubmit}
inputMin={1}
/>
);
};
@@ -9,6 +9,8 @@ interface JumpToProps {
inputType: HTMLInputTypeAttribute;
inputName: string;
submitHandler: (arg0: SyntheticEvent) => void;
inputMin?: string | number;
inputMax?: string | number;
}
export const JumpTo = ({
@@ -18,6 +20,8 @@ export const JumpTo = ({
inputType,
inputName,
submitHandler,
inputMin,
inputMax,
}: JumpToProps) => {
return (
<form onSubmit={submitHandler}>
@@ -35,6 +39,8 @@ export const JumpTo = ({
name={inputName}
placeholder={placeholder}
className="max-w-[200px]"
min={inputMin}
max={inputMax}
/>
<Button
data-testid="go-submit"
@@ -1,33 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
interface SecondsAgoProps {
date: string | undefined;
}
export const SecondsAgo = ({ date, ...props }: SecondsAgoProps) => {
const [now, setNow] = useState(Date.now());
useEffect(() => {
const int = setInterval(() => {
setNow(Date.now());
}, 500);
return () => clearInterval(int);
}, [setNow]);
if (!date) {
return <>{t('Date unknown')}</>;
}
const timeAgoInSeconds = Math.floor((now - new Date(date).getTime()) / 1000);
return (
<div {...props}>
{t(
`${
timeAgoInSeconds === 1 ? '1 second' : `${timeAgoInSeconds} seconds`
} ago`
)}
</div>
);
};
@@ -1,36 +0,0 @@
import { render, screen, act } from '@testing-library/react';
import { SecondsAgo } from './index';
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
describe('Seconds ago', () => {
it('should render successfully', () => {
const dateInString = new Date().toString();
render(<SecondsAgo data-testid="test-seconds-ago" date={dateInString} />);
expect(screen.getByTestId('test-seconds-ago')).toBeInTheDocument();
});
it('should show the correct amount of seconds ago', () => {
const secondsToWait = 10;
const dateInString = new Date().toString();
act(() => {
jest.advanceTimersByTime(secondsToWait * 1000);
});
jest.runOnlyPendingTimers();
render(<SecondsAgo data-testid="test-seconds-ago" date={dateInString} />);
expect(screen.getByTestId('test-seconds-ago')).toHaveTextContent(
`${secondsToWait} seconds ago`
);
});
});
@@ -0,0 +1,26 @@
import { formatDistanceToNowStrict } from 'date-fns';
import { t } from '@vegaprotocol/react-helpers';
import { useEffect, useState } from 'react';
interface TimeAgoProps {
date: string;
}
export const TimeAgo = ({ date, ...props }: TimeAgoProps) => {
const [distanceToNow, setDistanceToNow] = useState(
formatDistanceToNowStrict(new Date(date))
);
useEffect(() => {
const int = setInterval(() => {
date && setDistanceToNow(formatDistanceToNowStrict(new Date(date)));
}, 500);
return () => clearInterval(int);
}, [setDistanceToNow, date]);
if (!date) {
return <>{t('Date unknown')}</>;
}
return <span {...props}>{t(`${distanceToNow} ago`)}</span>;
};
@@ -0,0 +1,45 @@
import { render, screen, act } from '@testing-library/react';
import { TimeAgo } from './index';
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterEach(() => {
jest.useRealTimers();
});
describe('Time ago', () => {
it('should render successfully', () => {
const dateString = new Date().toString();
render(<TimeAgo data-testid="date" date={dateString} />);
expect(screen.getByTestId('date')).toBeInTheDocument();
});
it('should show the correct amount of time ago', () => {
const secondsToWait = 10;
const date = new Date(-(secondsToWait * 1000)).toString();
render(<TimeAgo data-testid="test-time-ago" date={date} />);
expect(screen.getByTestId('test-time-ago')).toHaveTextContent(
`${secondsToWait} seconds ago`
);
});
it('should show the correct amount of time ago after time has advanced', () => {
const date = new Date().toString();
render(<TimeAgo data-testid="test-time-elapsed" date={date} />);
act(() => {
jest.advanceTimersByTime(30000);
});
expect(screen.getByTestId('test-time-elapsed')).toHaveTextContent(
`30 seconds ago`
);
});
});
+1
View File
@@ -24,6 +24,7 @@ export const ENV = {
genesis: truthy.includes(windowOrDefault('NX_EXPLORER_GENESIS')),
governance: truthy.includes(windowOrDefault('NX_EXPLORER_GOVERNANCE')),
markets: truthy.includes(windowOrDefault('NX_EXPLORER_MARKETS')),
oracles: truthy.includes(windowOrDefault('NX_EXPLORER_ORACLES')),
txsList: truthy.includes(windowOrDefault('NX_EXPLORER_TXS_LIST')),
networkParameters: truthy.includes(
windowOrDefault('NX_EXPLORER_NETWORK_PARAMETERS')
@@ -174,7 +174,7 @@ describe('Block', () => {
);
expect(proposer).toHaveAttribute('href', `/${RouteNames.VALIDATORS}`);
expect(screen.getByTestId('block-time')).toHaveTextContent(
'3528 seconds ago'
'59 minutes ago'
);
});
@@ -1,9 +1,10 @@
import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { DATA_SOURCES } from '../../../config';
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
import type { TendermintBlocksResponse } from '../tendermint-blocks-response';
import { RouteTitle } from '../../../components/route-title';
import { SecondsAgo } from '../../../components/seconds-ago';
import { TimeAgo } from '../../../components/time-ago';
import {
TableWithTbody,
TableRow,
@@ -53,30 +54,38 @@ const Block = () => {
</Button>
</Link>
</div>
<TableWithTbody className="mb-28">
<TableRow modifier="bordered">
<TableHeader scope="row">Mined by</TableHeader>
<TableCell modifier="bordered">
<HighlightedLink
to={`/${Routes.VALIDATORS}`}
text={blockData?.result.block.header.proposer_address}
data-testid="block-validator"
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableHeader scope="row">Time</TableHeader>
<TableCell modifier="bordered">
<SecondsAgo
data-testid="block-time"
date={blockData?.result.block.header.time}
/>
</TableCell>
</TableRow>
</TableWithTbody>
{blockData && blockData.result.block.data.txs.length > 0 ? (
<TxsPerBlock blockHeight={block} />
) : null}
{blockData && (
<>
<TableWithTbody className="mb-28">
<TableRow modifier="bordered">
<TableHeader scope="row">Mined by</TableHeader>
<TableCell modifier="bordered">
<HighlightedLink
to={`/${Routes.VALIDATORS}`}
text={blockData.result.block.header.proposer_address}
data-testid="block-validator"
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableHeader scope="row">Time</TableHeader>
<TableCell modifier="bordered">
<TimeAgo
data-testid="block-time"
date={blockData.result.block.header.time}
/>{' '}
-{' '}
{getDateTimeFormat().format(
new Date(blockData.result.block.header.time)
)}
</TableCell>
</TableRow>
</TableWithTbody>
{blockData.result.block.data.txs.length > 0 ? (
<TxsPerBlock blockHeight={block} />
) : null}
</>
)}
</>
</RenderFetched>
</section>
@@ -108,7 +108,9 @@ const Governance = () => {
if (!data) return null;
return (
<section>
<RouteTitle data-testid="governance-header">{t('Governance')}</RouteTitle>
<RouteTitle data-testid="governance-header">
{t('Governance Proposals')}
</RouteTitle>
{data.proposals?.map((p) => (
<React.Fragment key={p.id}>
<SubHeading>{getProposalName(p.terms.change)}</SubHeading>
@@ -24,10 +24,10 @@ describe('NetworkParametersTable', () => {
);
const rows = screen.getAllByTestId('key-value-table-row');
expect(rows[0].children[0]).toHaveTextContent(
'Market Fee Factors Infrastructure Fee'
'market.fee.factors.infrastructureFee'
);
expect(rows[1].children[0]).toHaveTextContent(
'Market Liquidity Provision Min Lp Stake Quantum Multiple'
'market.liquidityProvision.minLpStakeQuantumMultiple'
);
expect(rows[0].children[1]).toHaveTextContent('0.0005');
expect(rows[1].children[1]).toHaveTextContent('1');
@@ -54,10 +54,10 @@ describe('NetworkParametersTable', () => {
);
const rows = screen.getAllByTestId('key-value-table-row');
expect(rows[0].children[0]).toHaveTextContent(
'Market Fee Factors Infrastructure Fee'
'market.fee.factors.infrastructureFee'
);
expect(rows[1].children[0]).toHaveTextContent(
'Market Liquidity Provision Min Lp Stake Quantum Multiple'
'market.liquidityProvision.minLpStakeQuantumMultiple'
);
expect(rows[0].children[1]).toHaveTextContent('0.0005');
expect(rows[1].children[1]).toHaveTextContent('1');
@@ -16,7 +16,6 @@ import type {
NetworkParametersQuery_networkParameters,
} from './__generated__/NetworkParametersQuery';
import orderBy from 'lodash/orderBy';
import startCase from 'lodash/startCase';
const BIG_NUMBER_PARAMS = [
'spam.protection.delegation.min.tokens',
@@ -42,8 +41,15 @@ export const renderRow = ({
}: NetworkParametersQuery_networkParameters) => {
const isSyntaxRow = isJsonObject(value);
return (
<KeyValueTableRow key={key} inline={!isSyntaxRow}>
{startCase(key)}
<KeyValueTableRow
key={key}
inline={!isSyntaxRow}
id={key}
className={
'group target:bg-vega-pink target:text-white dark:target:bg-vega-yellow dark:target:text-black'
}
>
{key}
{isSyntaxRow ? (
<SyntaxHighlighter data={JSON.parse(value)} />
) : isNaN(Number(value)) ? (
@@ -0,0 +1,97 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { OracleSpecStatus, PropertyKeyType, ConditionOperator } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: OracleSpecs
// ====================================================
export interface OracleSpecs_oracleSpecs_filters_key {
__typename: "PropertyKey";
/**
* name is the name of the property.
*/
name: string | null;
/**
* type is the type of the property.
*/
type: PropertyKeyType;
}
export interface OracleSpecs_oracleSpecs_filters_conditions {
__typename: "Condition";
/**
* value is used by the comparator.
*/
value: string | null;
/**
* comparator is the type of comparison to make on the value.
*/
operator: ConditionOperator;
}
export interface OracleSpecs_oracleSpecs_filters {
__typename: "Filter";
/**
* key is the oracle data property key targeted by the filter.
*/
key: OracleSpecs_oracleSpecs_filters_key;
/**
* conditions are the conditions that should be matched by the data to be
* considered of interest.
*/
conditions: OracleSpecs_oracleSpecs_filters_conditions[] | null;
}
export interface OracleSpecs_oracleSpecs_data {
__typename: "OracleData";
/**
* pubKeys is the list of public keys that signed the data
*/
pubKeys: string[] | null;
}
export interface OracleSpecs_oracleSpecs {
__typename: "OracleSpec";
/**
* status describes the status of the oracle spec
*/
status: OracleSpecStatus;
/**
* id is a hash generated from the OracleSpec data.
*/
id: string;
/**
* RFC3339Nano creation date time
*/
createdAt: string;
/**
* RFC3339Nano last updated timestamp
*/
updatedAt: string | null;
/**
* pubKeys is the list of authorized public keys that signed the data for this
* oracle. All the public keys in the oracle data should be contained in these
* public keys.
*/
pubKeys: string[] | null;
/**
* filters describes which oracle data are considered of interest or not for
* the product (or the risk model).
*/
filters: OracleSpecs_oracleSpecs_filters[] | null;
/**
* data list all the oracle data broadcast to this spec
*/
data: OracleSpecs_oracleSpecs_data[];
}
export interface OracleSpecs {
/**
* All registered oracle specs
*/
oracleSpecs: OracleSpecs_oracleSpecs[] | null;
}
@@ -0,0 +1,53 @@
import { gql, useQuery } from '@apollo/client';
import type { OracleSpecs as OracleSpecsQuery } from './__generated__/OracleSpecs';
import React from 'react';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { RouteTitle } from '../../components/route-title';
import { t } from '@vegaprotocol/react-helpers';
import { SubHeading } from '../../components/sub-heading';
const ORACLE_SPECS_QUERY = gql`
query OracleSpecs {
oracleSpecs {
status
id
createdAt
updatedAt
pubKeys
filters {
key {
name
type
}
conditions {
value
operator
}
}
data {
pubKeys
}
}
}
`;
const Oracles = () => {
const { data } = useQuery<OracleSpecsQuery>(ORACLE_SPECS_QUERY);
return (
<section>
<RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle>
{data?.oracleSpecs
? data.oracleSpecs.map((o) => (
<React.Fragment key={o.id}>
<SubHeading>{o.id}</SubHeading>
<SyntaxHighlighter data={o} />
</React.Fragment>
))
: null}
</section>
);
};
export default Oracles;
@@ -8,5 +8,6 @@ export const Routes = {
GENESIS: 'genesis',
GOVERNANCE: 'governance',
MARKETS: 'markets',
ORACLES: 'oracles',
NETWORK_PARAMETERS: 'network-parameters',
};
+15 -2
View File
@@ -3,6 +3,7 @@ import BlockPage from './blocks';
import Governance from './governance';
import Home from './home';
import Markets from './markets';
import Oracles from './oracles';
import Party from './parties';
import { Parties } from './parties/home';
import { Party as PartySingle } from './parties/id';
@@ -66,8 +67,8 @@ const governanceRoutes = flags.governance
? [
{
path: Routes.GOVERNANCE,
name: 'Governance',
text: t('Proposals'),
name: 'Governance proposals',
text: t('Governance Proposals'),
element: <Governance />,
},
]
@@ -84,6 +85,17 @@ const marketsRoutes = flags.markets
]
: [];
const oraclesRoutes = flags.oracles
? [
{
path: Routes.ORACLES,
name: 'Oracles',
text: t('Oracles'),
element: <Oracles />,
},
]
: [];
const networkParametersRoutes = flags.networkParameters
? [
{
@@ -154,6 +166,7 @@ const routerConfig = [
...genesisRoutes,
...governanceRoutes,
...marketsRoutes,
...oraclesRoutes,
...networkParametersRoutes,
...validators,
];
+1
View File
@@ -24,3 +24,4 @@ NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
NX_VEGA_ENV=TESTNET
NX_VEGA_REST=https://lb.testnet.vega.xyz/datanode/rest
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
@@ -18,4 +18,22 @@ module.exports = defineConfig({
viewportWidth: 1440,
viewportHeight: 900,
},
env: {
TRADING_TEST_VEGA_WALLET_NAME: 'UI_Trading_Test',
ETHEREUM_PROVIDER_URL:
'https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8',
VEGA_PUBLIC_KEY:
'47836c253520d2661bf5bed6339c0de08fd02cf5d4db0efee3b4373f20c7d278',
VEGA_PUBLIC_KEY2:
'1a18cdcaaa4f44a57b35a4e9b77e0701c17a476f2b407620f8c17371740cf2e4',
TRUNCATED_VEGA_PUBLIC_KEY: '47836c…c7d278',
TRUNCATED_VEGA_PUBLIC_KEY2: '1a18cd…0cf2e4',
ETHEREUM_WALLET_ADDRESS: '0x265Cc6d39a1B53d0d92068443009eE7410807158',
ETHERSCAN_URL: 'https://ropsten.etherscan.io',
tsConfig: 'tsconfig.json',
TAGS: 'not @todo and not @ignore and not @manual',
TRADING_TEST_VEGA_WALLET_PASSPHRASE: '123',
ETH_WALLET_MNEMONIC:
'ugly gallery notice network true range brave clarify flat logic someone chunk',
},
});
@@ -1,6 +1,22 @@
import { aliasQuery } from '@vegaprotocol/cypress';
import { generateSimpleMarkets } from '../support/mocks/generate-markets';
import { generateDealTicket } from '../support/mocks/generate-deal-ticket';
import { generateMarketTags } from '../support/mocks/generate-market-tags';
import { generateMarketPositions } from '../support/mocks/generate-market-positions';
import { generateEstimateOrder } from '../support/mocks/generate-estimate-order';
import { generatePartyBalance } from '../support/mocks/generate-party-balance';
const connectVegaWallet = () => {
const form = 'rest-connector-form';
const walletName = Cypress.env('TRADING_TEST_VEGA_WALLET_NAME');
const walletPassphrase = Cypress.env('TRADING_TEST_VEGA_WALLET_PASSPHRASE');
cy.getByTestId('connect-vega-wallet').click();
cy.getByTestId('connectors-list').find('button').click();
cy.getByTestId(form).find('#wallet').click().type(walletName);
cy.getByTestId(form).find('#passphrase').click().type(walletPassphrase);
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
};
describe('Market trade', () => {
let markets;
@@ -8,6 +24,10 @@ describe('Market trade', () => {
cy.mockGQL((req) => {
aliasQuery(req, 'SimpleMarkets', generateSimpleMarkets());
aliasQuery(req, 'DealTicketQuery', generateDealTicket());
aliasQuery(req, 'MarketTags', generateMarketTags());
aliasQuery(req, 'MarketPositions', generateMarketPositions());
aliasQuery(req, 'EstimateOrder', generateEstimateOrder());
aliasQuery(req, 'PartyBalanceQuery', generatePartyBalance());
});
cy.visit('/markets');
cy.wait('@SimpleMarkets').then((response) => {
@@ -64,4 +84,32 @@ describe('Market trade', () => {
);
}
});
it('order review should display proper calculations', () => {
if (markets?.length) {
cy.visit(`/trading/${markets[0].id}`);
connectVegaWallet();
cy.get('h3').contains('Review Trade').click();
cy.getByTestId('key-value-table')
.find('dl')
.eq(1)
.find('dd div')
.should('have.text', '25.78726');
cy.getByTestId('key-value-table')
.find('dl')
.eq(2)
.find('dd div')
.should('have.text', '1.00000');
cy.getByTestId('key-value-table')
.find('dl')
.eq(3)
.find('dd div')
.should('have.text', '-785.81045');
cy.getByTestId('place-order').click();
cy.getByTestId('dialog-title').should(
'have.text',
'Confirm transaction in wallet'
);
}
});
});
@@ -0,0 +1,12 @@
export const generateEstimateOrder = () => {
return {
estimateOrder: {
totalFeeAmount: '16085.09240212.7380425.46',
marginLevels: {
initialLevel: '2844054.80937741220203',
__typename: 'MarginLevels',
},
__typename: 'OrderEstimate',
},
};
};
@@ -0,0 +1,57 @@
export const generateMarketPositions = () => {
return {
party: {
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
accounts: [
{
asset: {
decimals: 5,
},
balance: '400000000000000000000',
market: {
id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221',
__typename: 'Market',
},
},
{
asset: {
decimals: 5,
},
balance: '265329',
market: {
id: 'first-btcusd-id',
__typename: 'Market',
},
},
],
positionsConnection: {
edges: [
{
node: {
openVolume: '3',
market: {
id: '2751c508f9759761f912890f37fb3f97a00300bf7685c02a56a86e05facfe221',
__typename: 'Market',
},
__typename: 'Position',
},
__typename: 'PositionEdge',
},
{
node: {
openVolume: '12',
market: {
id: 'first-btcusd-id',
__typename: 'Market',
},
__typename: 'Position',
},
__typename: 'PositionEdge',
},
],
__typename: 'PositionConnection',
},
__typename: 'Party',
},
};
};
@@ -0,0 +1,25 @@
export const generateMarketTags = () => {
return {
market: {
tradableInstrument: {
instrument: {
metadata: {
tags: [
'formerly:2839D9B2329C9E70',
'base:AAVE',
'quote:DAI',
'class:fx/crypto',
'monthly',
'sector:defi',
'settlement:2022-08-01',
],
__typename: 'InstrumentMetadata',
},
__typename: 'Instrument',
},
__typename: 'TradableInstrument',
},
__typename: 'Market',
},
};
};
@@ -0,0 +1,53 @@
export const generatePartyBalance = () => {
return {
party: {
accounts: [
{
balance: '88474051',
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
name: 'tDAI TEST',
decimals: 5,
__typename: 'Asset',
},
__typename: 'Account',
},
{
balance: '100000000',
asset: {
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
symbol: 'tEURO',
name: 'tEURO TEST',
decimals: 5,
__typename: 'Asset',
},
__typename: 'Account',
},
{
balance: '3412867',
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
name: 'tDAI TEST',
decimals: 5,
__typename: 'Asset',
},
__typename: 'Account',
},
{
balance: '70007',
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
name: 'tDAI TEST',
decimals: 5,
__typename: 'Asset',
},
__typename: 'Account',
},
],
__typename: 'Party',
},
};
};
+1
View File
@@ -21,3 +21,4 @@ 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://lb.testnet.vega.xyz/query"
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
@@ -0,0 +1,51 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: MarketTags
// ====================================================
export interface MarketTags_market_tradableInstrument_instrument_metadata {
__typename: "InstrumentMetadata";
/**
* An arbitrary list of tags to associated to associate to the Instrument (string list)
*/
tags: string[] | null;
}
export interface MarketTags_market_tradableInstrument_instrument {
__typename: "Instrument";
/**
* Metadata for this instrument
*/
metadata: MarketTags_market_tradableInstrument_instrument_metadata;
}
export interface MarketTags_market_tradableInstrument {
__typename: "TradableInstrument";
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: MarketTags_market_tradableInstrument_instrument;
}
export interface MarketTags_market {
__typename: "Market";
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: MarketTags_market_tradableInstrument;
}
export interface MarketTags {
/**
* An instrument that is trading on the VEGA network
*/
market: MarketTags_market | null;
}
export interface MarketTagsVariables {
marketId: string;
}
@@ -61,7 +61,7 @@ export const DealTicketContainer = () => {
isWalletConnected={!!keypair?.pub}
/>
)}
<DealTicketSteps market={data.market} />
<DealTicketSteps market={data.market} partyData={partyData} />
</DealTicketManager>
)}
</Container>
@@ -1,9 +1,16 @@
import * as React from 'react';
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import { DealTicketAmount, MarketSelector } from '@vegaprotocol/deal-ticket';
import { InputError } from '@vegaprotocol/ui-toolkit';
import {
DealTicketAmount,
getDialogTitle,
getDialogIntent,
getDialogIcon,
MarketSelector,
} from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
@@ -11,17 +18,22 @@ import {
getDefaultOrder,
useOrderValidation,
useOrderSubmit,
OrderFeedback,
} from '@vegaprotocol/orders';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';
import SideSelector, { SIDE_NAMES } from './side-selector';
import ReviewTrade from './review-trade';
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
interface DealTicketMarketProps {
market: DealTicketQuery_market;
partyData?: PartyBalanceQuery;
}
export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
export const DealTicketSteps = ({
market,
partyData,
}: DealTicketMarketProps) => {
const navigate = useNavigate();
const setMarket = useCallback(
(marketId) => {
@@ -45,6 +57,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const orderSide = watch('side');
const order = watch();
const { message: invalidText, isDisabled } = useOrderValidation({
step,
@@ -54,7 +67,8 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
fieldErrors: errors,
});
const { submit, transaction } = useOrderSubmit(market);
const { submit, transaction, finalizedOrder, TransactionDialog } =
useOrderSubmit(market);
const transactionStatus =
transaction.status === VegaTxStatus.Requested ||
@@ -101,7 +115,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
component: (
<DealTicketAmount
orderType={orderType}
step={0.02}
step={step}
register={register}
price={
market.depth.lastTrade
@@ -121,17 +135,20 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
{invalidText}
</InputError>
)}
<Button
className="w-full mb-8"
variant="primary"
type="submit"
disabled={transactionStatus === 'pending' || isDisabled}
data-testid="place-order"
<ReviewTrade
market={market}
isDisabled={isDisabled}
transactionStatus={transactionStatus}
order={order}
partyData={partyData}
/>
<TransactionDialog
title={getDialogTitle(finalizedOrder?.status)}
intent={getDialogIntent(finalizedOrder?.status)}
icon={getDialogIcon(finalizedOrder?.status)}
>
{transactionStatus === 'pending'
? t('Pending...')
: t('Place order')}
</Button>
<OrderFeedback transaction={transaction} order={finalizedOrder} />
</TransactionDialog>
</div>
),
disabled: true,
@@ -0,0 +1,146 @@
import { addDecimal, formatNumber, t } from '@vegaprotocol/react-helpers';
import {
Button,
Icon,
KeyValueTable,
KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit';
import * as React from 'react';
import classNames from 'classnames';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { Order } from '@vegaprotocol/orders';
import { SIDE_NAMES } from './side-selector';
import { useVegaWallet, VegaWalletOrderSide } from '@vegaprotocol/wallet';
import SimpleMarketExpires from '../simple-market-list/simple-market-expires';
import { gql, useQuery } from '@apollo/client';
import type {
MarketTags,
MarketTagsVariables,
} from './__generated__/MarketTags';
import useOrderMargin from '../../hooks/use-order-margin';
import useOrderCloseOut from '../../hooks/use-order-closeout';
import { IconNames } from '@blueprintjs/icons';
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
export const MARKET_TAGS_QUERY = gql`
query MarketTags($marketId: ID!) {
market(id: $marketId) {
tradableInstrument {
instrument {
metadata {
tags
}
}
}
}
}
`;
interface Props {
market: DealTicketQuery_market;
isDisabled: boolean;
transactionStatus?: string;
order: Order;
partyData?: PartyBalanceQuery;
}
export default ({
isDisabled,
market,
order,
transactionStatus,
partyData,
}: Props) => {
const { keypair } = useVegaWallet();
const { data: tagsData } = useQuery<MarketTags, MarketTagsVariables>(
MARKET_TAGS_QUERY,
{
variables: { marketId: market.id },
}
);
const estMargin = useOrderMargin({
order,
market,
partyId: keypair?.pub || '',
});
const estCloseOut = useOrderCloseOut({ order, market, partyData });
return (
<div className="mb-8 text-black dark:text-white">
<KeyValueTable>
<KeyValueTableRow noBorder>
<div className="flex flex-none gap-x-5 items-center">
<div
className={classNames(
{
'buyButton dark:buyButtonDark':
order.side === VegaWalletOrderSide.Buy,
'sellButton dark:sellButtonDark':
order.side === VegaWalletOrderSide.Sell,
},
'px-8 py-4 inline text-ui-small'
)}
>
{SIDE_NAMES[order.side]}
</div>
<div>{market.tradableInstrument.instrument.product.quoteName}</div>
<div>
{tagsData?.market?.tradableInstrument.instrument.metadata
.tags && (
<SimpleMarketExpires
tags={
tagsData?.market.tradableInstrument.instrument.metadata.tags
}
/>
)}
</div>
</div>
<div className="text-blue">
@{' '}
{market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: ' - '}{' '}
<span className="text-ui-small inline">(EST)</span>
</div>
</KeyValueTableRow>
<KeyValueTableRow noBorder>
<>{t('Est. margin')}</>
<div className="text-black dark:text-white flex gap-x-5 items-center">
{estMargin}
<Icon name={IconNames.ISSUE} className="rotate-180" />
</div>
</KeyValueTableRow>
<KeyValueTableRow noBorder>
<>
{t('Size')}{' '}
<div className="text-ui-small inline">
({market.tradableInstrument.instrument.product.quoteName})
</div>
</>
<div className="text-black dark:text-white flex gap-x-5 items-center">
{formatNumber(order.size, market.decimalPlaces)}
<Icon name={IconNames.ISSUE} className="rotate-180" />
</div>
</KeyValueTableRow>
<KeyValueTableRow noBorder>
<>{t('Est. close out')}</>
<div className="text-black dark:text-white flex gap-x-5 items-center">
{estCloseOut}
<Icon name={IconNames.ISSUE} className="rotate-180" />
</div>
</KeyValueTableRow>
</KeyValueTable>
<Button
className="w-full !py-8 mt-64 max-w-sm"
boxShadow={false}
variant="secondary"
type="submit"
disabled={transactionStatus === 'pending' || isDisabled}
data-testid="place-order"
appendIconName="arrow-top-right"
>
{transactionStatus === 'pending' ? t('Pending...') : t('Submit')}
</Button>
</div>
);
};
@@ -29,6 +29,7 @@ export default ({ value, onSelect }: SideSelectorProps) => {
variant="inline-link"
aria-label={t('Open long position')}
className={classNames(
'py-8',
'buyButton hover:buyButton dark:buyButtonDark dark:hover:buyButtonDark',
{ selected: value === VegaWalletOrderSide.Buy }
)}
@@ -40,6 +41,7 @@ export default ({ value, onSelect }: SideSelectorProps) => {
variant="inline-link"
aria-label={t('Open short position')}
className={classNames(
'py-8',
'sellButton hover:sellButton dark:sellButtonDark dark:hover:sellButtonDark',
{ selected: value === VegaWalletOrderSide.Sell }
)}
@@ -23,7 +23,7 @@ describe('SimpleMarketExpires', () => {
'settlement-date:2022-04-25T1200',
];
render(<SimpleMarketExpires tags={tags} />);
expect(screen.getByText('April 25')).toBeInTheDocument();
expect(screen.getByText('Apr 25')).toBeInTheDocument();
});
it('last one proper tag should matter', () => {
@@ -33,7 +33,7 @@ describe('SimpleMarketExpires', () => {
'settlement-expiry-date:2022-03-25T12:00:00',
];
render(<SimpleMarketExpires tags={tags} />);
expect(screen.getByText('March 25')).toBeInTheDocument();
expect(screen.getByText('Mar 25')).toBeInTheDocument();
});
it('when no proper tag nor date should be null', () => {
@@ -1,7 +1,7 @@
import type { SimpleMarkets_markets } from '../components/simple-market-list/__generated__/SimpleMarkets';
export const DATE_FORMAT = 'dd MMMM yyyy HH:mm';
export const EXPIRE_DATE_FORMAT = 'MMMM dd';
export const EXPIRE_DATE_FORMAT = 'MMM dd';
export const TRADABLE_STATES = {
Active: true,
@@ -0,0 +1,48 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { Side, OrderTimeInForce, OrderType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: EstimateOrder
// ====================================================
export interface EstimateOrder_estimateOrder_marginLevels {
__typename: "MarginLevels";
/**
* this is the minimal margin required for a party to place a new order on the network (unsigned int actually)
*/
initialLevel: string;
}
export interface EstimateOrder_estimateOrder {
__typename: "OrderEstimate";
/**
* The total estimated amount of fee if the order was to trade
*/
totalFeeAmount: string;
/**
* The margin requirement for this order
*/
marginLevels: EstimateOrder_estimateOrder_marginLevels;
}
export interface EstimateOrder {
/**
* return an estimation of the potential cost for a new order
*/
estimateOrder: EstimateOrder_estimateOrder;
}
export interface EstimateOrderVariables {
marketId: string;
partyId: string;
price?: string | null;
size: string;
side: Side;
timeInForce: OrderTimeInForce;
expiration?: string | null;
type: OrderType;
}
@@ -0,0 +1,106 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: MarketPositions
// ====================================================
export interface MarketPositions_party_accounts_asset {
__typename: "Asset";
/**
* The precision of the asset
*/
decimals: number;
}
export interface MarketPositions_party_accounts_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface MarketPositions_party_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
balance: string;
/**
* Asset, the 'currency'
*/
asset: MarketPositions_party_accounts_asset;
/**
* Market (only relevant to margin accounts)
*/
market: MarketPositions_party_accounts_market | null;
}
export interface MarketPositions_party_positionsConnection_edges_node_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface MarketPositions_party_positionsConnection_edges_node {
__typename: "Position";
/**
* Open volume (uint64)
*/
openVolume: string;
/**
* Market relating to this position
*/
market: MarketPositions_party_positionsConnection_edges_node_market;
}
export interface MarketPositions_party_positionsConnection_edges {
__typename: "PositionEdge";
node: MarketPositions_party_positionsConnection_edges_node;
}
export interface MarketPositions_party_positionsConnection {
__typename: "PositionConnection";
/**
* The positions in this connection
*/
edges: MarketPositions_party_positionsConnection_edges[] | null;
}
export interface MarketPositions_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Collateral accounts relating to a party
*/
accounts: MarketPositions_party_accounts[] | null;
/**
* Trading positions relating to a party
*/
positionsConnection: MarketPositions_party_positionsConnection;
}
export interface MarketPositions {
/**
* An entity that is trading on the VEGA network
*/
party: MarketPositions_party | null;
}
export interface MarketPositionsVariables {
partyId: string;
}
@@ -0,0 +1,133 @@
import { renderHook } from '@testing-library/react-hooks';
import useMarketPositions from './use-market-positions';
let mockNotEmptyData = {
party: {
accounts: [
{
balance: '50001000000',
asset: {
decimals: 5,
},
market: {
id: 'marketId',
},
},
{
balance: '700000000000000000000000000000',
asset: {
decimals: 5,
},
market: {
id: 'someOtherMarketId',
},
},
],
positionsConnection: {
edges: [
{
node: {
openVolume: '100002',
market: {
id: 'marketId',
},
},
},
{
node: {
openVolume: '3',
market: {
id: 'someOtherMarketId',
},
},
},
],
},
},
};
jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(() => ({ data: mockNotEmptyData })),
}));
describe('useOrderPosition Hook', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper positive value', () => {
const { result } = renderHook(() =>
useMarketPositions({ marketId: 'marketId', partyId: 'partyId' })
);
expect(result.current?.openVolume.toNumber()).toEqual(100002);
expect(result.current?.balance.toString()).toEqual('50001000000');
});
it('if balance equal 0 return null', () => {
mockNotEmptyData = {
party: {
accounts: [
{
balance: '0',
asset: {
decimals: 5,
},
market: {
id: 'marketId',
},
},
],
positionsConnection: {
edges: [
{
node: {
openVolume: '2',
market: {
id: 'marketId',
},
},
},
],
},
},
};
const { result } = renderHook(() =>
useMarketPositions({ marketId: 'marketId', partyId: 'partyId' })
);
expect(result.current).toBeNull();
});
it('if no markets return null', () => {
mockNotEmptyData = {
party: {
accounts: [
{
balance: '33330',
asset: {
decimals: 5,
},
market: {
id: 'otherMarketId',
},
},
],
positionsConnection: {
edges: [
{
node: {
openVolume: '2',
market: {
id: 'otherMarketId',
},
},
},
],
},
},
};
const { result } = renderHook(() =>
useMarketPositions({ marketId: 'marketId', partyId: 'partyId' })
);
expect(result.current).toBeNull();
});
});
@@ -0,0 +1,77 @@
import { gql, useQuery } from '@apollo/client';
import { BigNumber } from 'bignumber.js';
import type {
MarketPositions,
MarketPositionsVariables,
} from './__generated__/marketPositions';
const MARKET_POSITIONS_QUERY = gql`
query MarketPositions($partyId: ID!) {
party(id: $partyId) {
id
accounts {
type
balance
asset {
decimals
}
market {
id
}
}
positionsConnection {
edges {
node {
openVolume
market {
id
}
}
}
}
}
}
`;
interface Props {
marketId: string;
partyId: string;
}
export type PositionMargin = {
openVolume: BigNumber;
balance: BigNumber;
balanceDecimals?: number;
} | null;
export default ({ marketId, partyId }: Props): PositionMargin => {
const { data } = useQuery<MarketPositions, MarketPositionsVariables>(
MARKET_POSITIONS_QUERY,
{
pollInterval: 5000,
variables: { partyId },
skip: !partyId,
}
);
const account = data?.party?.accounts?.find(
(nodes) => nodes.market?.id === marketId
);
if (account) {
const balance = new BigNumber(account.balance || 0);
const openVolume = new BigNumber(
data?.party?.positionsConnection?.edges?.find(
(nodes) => nodes.node.market.id === marketId
)?.node.openVolume || 0
);
if (!balance.isZero() && !openVolume.isZero()) {
return {
balance,
balanceDecimals: account?.asset.decimals,
openVolume,
};
}
}
return null;
};
@@ -0,0 +1,71 @@
import { renderHook } from '@testing-library/react-hooks';
import useOrderCloseOut from './use-order-closeout';
import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
describe('useOrderCloseOut Hook', () => {
const order = { size: '2', side: 'SIDE_BUY' };
const market = {
decimalPlaces: 5,
depth: {
lastTrade: {
price: '1000000',
},
},
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'assetId',
},
},
},
},
};
const partyData = {
party: {
accounts: [
{
balance: '200000',
asset: {
id: 'assetId',
decimals: 5,
},
},
],
},
};
it('return proper buy value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: order as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
})
);
expect(result.current).toEqual('9.00000');
});
it('return proper sell value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
partyData: partyData as PartyBalanceQuery,
})
);
expect(result.current).toEqual('11.00000');
});
it('return proper empty value', () => {
const { result } = renderHook(() =>
useOrderCloseOut({
order: { ...order, side: 'SIDE_SELL' } as Order,
market: market as DealTicketQuery_market,
})
);
expect(result.current).toEqual(' - ');
});
});
@@ -0,0 +1,39 @@
import { BigNumber } from 'bignumber.js';
import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { useSettlementAccount } from './use-settlement-account';
import { VegaWalletOrderSide } from '@vegaprotocol/wallet';
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers';
interface Props {
order: Order;
market: DealTicketQuery_market;
partyData?: PartyBalanceQuery;
}
const useOrderCloseOut = ({ order, market, partyData }: Props): string => {
const account = useSettlementAccount(
market.tradableInstrument.instrument.product.settlementAsset.id,
partyData?.party?.accounts || []
);
if (account?.balance && market.depth.lastTrade) {
const price = new BigNumber(
addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
);
const balance = new BigNumber(
addDecimal(account.balance, account.asset.decimals)
);
const { size, side } = order;
const bigOne = new BigNumber(1);
return formatNumber(
side === VegaWalletOrderSide.Buy
? bigOne.minus(balance.div(price.times(size))).times(price)
: bigOne.plus(balance.div(price.times(size))).times(price),
market.decimalPlaces
);
}
return ' - ';
};
export default useOrderCloseOut;
@@ -0,0 +1,106 @@
import { renderHook } from '@testing-library/react-hooks';
import { useQuery } from '@apollo/client';
import { BigNumber } from 'bignumber.js';
import type { Order } from '@vegaprotocol/orders';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { PositionMargin } from './use-market-positions';
import useOrderMargin from './use-order-margin';
let mockEstimateData = {
estimateOrder: {
marginLevels: {
initialLevel: '200000',
},
},
};
jest.mock('@apollo/client', () => ({
...jest.requireActual('@apollo/client'),
useQuery: jest.fn(() => ({ data: mockEstimateData })),
}));
let mockMarketPositions: PositionMargin = {
openVolume: new BigNumber(1),
balance: new BigNumber(100000),
};
jest.mock('./use-market-positions', () => jest.fn(() => mockMarketPositions));
describe('useOrderMargin Hook', () => {
const order = {
size: '2',
side: 'SIDE_BUY',
timeInForce: 'TIME_IN_FORCE_IOC',
type: 'TYPE_MARKET',
};
const market = {
id: 'marketId',
depth: {
lastTrade: {
price: '1000000',
},
},
};
const partyId = 'partyId';
afterEach(() => {
jest.clearAllMocks();
});
it('margin should be properly calculated', () => {
const { result } = renderHook(() =>
useOrderMargin({
order: order as Order,
market: market as DealTicketQuery_market,
partyId,
})
);
expect(result.current).toEqual('100000');
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)
.toString();
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
calledSize
);
});
it('if there is no positions initialMargin should not be subtracted', () => {
mockMarketPositions = null;
const { result } = renderHook(() =>
useOrderMargin({
order: order as Order,
market: market as DealTicketQuery_market,
partyId,
})
);
expect(result.current).toEqual('200000');
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
order.size
);
});
it('if api fails, should return empty value', () => {
mockEstimateData = {
estimateOrder: {
marginLevels: {
initialLevel: '',
},
},
};
const { result } = renderHook(() =>
useOrderMargin({
order: order as Order,
market: market as DealTicketQuery_market,
partyId,
})
);
expect(result.current).toEqual(' - ');
const calledSize = new BigNumber(mockMarketPositions?.openVolume || 0)
.plus(order.size)
.toString();
expect((useQuery as jest.Mock).mock.calls[0][1].variables.size).toEqual(
calledSize
);
});
});
@@ -0,0 +1,103 @@
import { BigNumber } from 'bignumber.js';
import type { Order } from '@vegaprotocol/orders';
import { gql, useQuery } from '@apollo/client';
import type {
EstimateOrder,
EstimateOrderVariables,
} from './__generated__/estimateOrder';
import type { DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import { OrderTimeInForce, OrderType, Side } from '@vegaprotocol/types';
import {
VegaWalletOrderSide,
VegaWalletOrderTimeInForce,
VegaWalletOrderType,
} from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/react-helpers';
import useMarketPositions from './use-market-positions';
export const ESTIMATE_ORDER_QUERY = gql`
query EstimateOrder(
$marketId: ID!
$partyId: ID!
$price: String
$size: String!
$side: Side!
$timeInForce: OrderTimeInForce!
$expiration: String
$type: OrderType!
) {
estimateOrder(
marketId: $marketId
partyId: $partyId
price: $price
size: $size
side: $side
timeInForce: $timeInForce
expiration: $expiration
type: $type
) {
totalFeeAmount
marginLevels {
initialLevel
}
}
}
`;
interface Props {
order: Order;
market: DealTicketQuery_market;
partyId: string;
}
const times: Record<VegaWalletOrderTimeInForce, OrderTimeInForce> = {
[VegaWalletOrderTimeInForce.GTC]: OrderTimeInForce.GTC,
[VegaWalletOrderTimeInForce.GTT]: OrderTimeInForce.GTT,
[VegaWalletOrderTimeInForce.IOC]: OrderTimeInForce.IOC,
[VegaWalletOrderTimeInForce.FOK]: OrderTimeInForce.FOK,
[VegaWalletOrderTimeInForce.GFN]: OrderTimeInForce.GFN,
[VegaWalletOrderTimeInForce.GFA]: OrderTimeInForce.GFA,
};
const types: Record<VegaWalletOrderType, OrderType> = {
[VegaWalletOrderType.Market]: OrderType.Market,
[VegaWalletOrderType.Limit]: OrderType.Limit,
};
const useOrderMargin = ({ order, market, partyId }: Props) => {
const marketPositions = useMarketPositions({ marketId: market.id, partyId });
const { data } = useQuery<EstimateOrder, EstimateOrderVariables>(
ESTIMATE_ORDER_QUERY,
{
variables: {
marketId: market.id,
partyId,
price: market.depth.lastTrade?.price,
size: new BigNumber(marketPositions?.openVolume || 0)
[order.side === VegaWalletOrderSide.Buy ? 'plus' : 'minus'](
order.size
)
.toString(),
side: order.side === VegaWalletOrderSide.Buy ? Side.Buy : Side.Sell,
timeInForce: times[order.timeInForce],
type: types[order.type],
},
skip:
!partyId || !market.id || !order.size || !market.depth.lastTrade?.price,
}
);
if (data?.estimateOrder.marginLevels.initialLevel) {
return addDecimal(
BigNumber.maximum(
0,
new BigNumber(data.estimateOrder.marginLevels.initialLevel).minus(
marketPositions?.balance || 0
)
).toString(),
market.decimalPlaces
);
}
return ' - ';
};
export default useOrderMargin;
File diff suppressed because it is too large Load Diff
@@ -38,7 +38,7 @@
"tranche_end": "2022-11-26T13:48:10.000Z",
"total_added": "100",
"total_removed": "0",
"locked_amount": "33.64974949264333",
"locked_amount": "31.73197932521563",
"deposits": [
{
"amount": "100",
@@ -242,7 +242,7 @@
"tranche_end": "2022-10-12T00:53:20.000Z",
"total_added": "1100",
"total_removed": "673.04388635",
"locked_amount": "232.909199010654485",
"locked_amount": "211.813727168949774",
"deposits": [
{
"amount": "1000",
+1 -1
View File
@@ -69,7 +69,7 @@
"tranche_end": "2022-10-12T00:53:20.000Z",
"total_added": "1010.000000000000000001",
"total_removed": "668.4622323651",
"locked_amount": "213.85302384576360370021173566717402337",
"locked_amount": "194.48351312785388340019255793378995434",
"deposits": [
{
"amount": "1000",
+2 -1
View File
@@ -12,7 +12,8 @@ NX_ETHEREUM_CHAIN_ID=1440
NX_ETH_URL_CONNECT=1
NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
#Test configuration variables
CYPRESS_FAIRGROUND=false
CYPRESS_INCLUDE_FLOWS=true
CYPRESS_INCLUDE_FLOWS=true
@@ -25,7 +25,8 @@ const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const txTimeout = { timeout: 40000 };
const epochTimeout = { timeout: 10000 };
context('Staking Tab - with eth and vega wallets connected', function () {
// Tests skipped because of change of the UI of data nodes
context.skip('Staking Tab - with eth and vega wallets connected', function () {
before('visit staking tab and connect vega wallet', function () {
cy.vega_wallet_import();
cy.visit('/');
@@ -1,4 +1,4 @@
const validatorList = '[data-testid="node-list-item-name"]';
const validatorsGrid = '[data-testid="validators-grid"]';
const ethWalletContainer = '[data-testid="ethereum-wallet"]';
const ethWalletAssociatedBalances =
'[data-testid="eth-wallet-associated-balances"]';
@@ -23,7 +23,7 @@ context(
cy.ethereum_wallet_connect();
cy.navigate_to('staking');
cy.wait_for_spinner();
cy.get(validatorList).first().invoke('text').as('validatorName');
cy.get(validatorsGrid).should('be.visible');
});
describe('Eth wallet - contains VEGA tokens', function () {
@@ -0,0 +1,287 @@
const walletContainer = '[data-testid="vega-wallet"]';
const walletHeader = '[data-testid="wallet-header"] h1';
const connectButton = '[data-testid="connect-vega"]';
const getVegaLink = '[data-testid="link"]';
const dialog = '[role="dialog"]';
const dialogHeader = '[data-testid="dialog-title"]';
const connectorsList = '[data-testid="connectors-list"]';
const dialogCloseBtn = '[data-testid="dialog-close"]';
const restConnectorForm = '[data-testid="rest-connector-form"]';
const restUrl = '#url';
const restWallet = '#wallet';
const restPassphrase = '#passphrase';
const restConnectBtn = '[type="submit"]';
const accountNo = '[data-testid="vega-account-truncated"]';
const walletName = '[data-testid="wallet-name"]';
const currencyTitle = '[data-testid="currency-title"]';
const currencyValue = '[data-testid="currency-value"]';
const vegaUnstaked = '[data-testid="vega-wallet-balance-unstaked"] .text-right';
const governanceBtn = '[href="/governance"]';
const stakingBtn = '[href="/staking"]';
const manageLink = '[data-testid="manage-vega-wallet"]';
const dialogWalletName = `[data-testid="key-${Cypress.env(
'vegaWalletPublicKey'
)}"] h2`;
const dialogVegaKey = '[data-testid="vega-public-key-full"]';
const dialogDisconnectBtn = '[data-testid="disconnect"]';
const copyPublicKeyBtn = '[data-testid="copy-vega-public-key"]';
context('Vega Wallet - verify elements on widget', function () {
before('visit token home page', function () {
cy.visit('/');
});
describe('with wallets disconnected', function () {
before('wait for widget to load', function () {
cy.get(walletContainer, { timeout: 10000 }).should('be.visible');
});
it('should have VEGA WALLET header visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletHeader)
.should('be.visible')
.and('have.text', 'Vega Wallet');
});
});
it('should have Connect Vega button visible', function () {
cy.get(walletContainer).within(() => {
cy.get(connectButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet to use associated $VEGA');
});
});
it('should have Get a Vega wallet link visible', function () {
cy.get(walletContainer).within(() => {
cy.get(getVegaLink)
.should('be.visible')
.and('have.text', 'Get a Vega wallet')
.and('have.attr', 'href', 'https://vega.xyz/wallet');
});
});
});
describe('when connect button clicked', function () {
before('click connect vega wallet button', function () {
cy.get(walletContainer).within(() => {
cy.get(connectButton).click();
});
});
it('should have Connect Vega header visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogHeader)
.should('be.visible')
.and('have.text', 'Connect to your Vega Wallet');
});
});
it('should have REST connector visible on list', function () {
cy.get(connectorsList).within(() => {
cy.get('button').should('be.visible').and('have.text', 'rest provider');
});
});
it('should have close button visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogCloseBtn).should('be.visible');
});
});
});
describe('when rest connector form opened', function () {
before('click rest provider link', function () {
cy.get(connectorsList).within(() => {
cy.get('button').click();
});
});
it('should have url field visible', function () {
cy.get(restConnectorForm).within(() => {
cy.get(restUrl)
.should('be.visible')
.and('have.value', 'http://localhost:1789/api/v1');
});
});
it('should have wallet field visible', function () {
cy.get(restConnectorForm).within(() => {
cy.get(restWallet).should('be.visible');
});
});
it('should have password field visible', function () {
cy.get(restConnectorForm).within(() => {
cy.get(restPassphrase).should('be.visible');
});
});
it('should have connect button visible', function () {
cy.get(restConnectorForm).within(() => {
cy.get(restConnectBtn).should('be.visible').and('have.text', 'Connect');
});
});
it('should have Connect Vega header visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogHeader)
.should('be.visible')
.and('have.text', 'Connect to your Vega Wallet');
});
});
it('should have close button visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogCloseBtn).should('be.visible');
});
});
// after('close dialog', function () {
// cy.get(dialogCloseBtn).click().should('not.exist');
// }); - to be changed when dialog state is fixed - https://github.com/vegaprotocol/frontend-monorepo/issues/838
});
describe('when vega wallet connected', function () {
before('connect vega wallet', function () {
cy.vega_wallet_import();
// cy.vega_wallet_connect(); - to be changed when dialog state is fixed - https://github.com/vegaprotocol/frontend-monorepo/issues/838
// then code below can be removed
cy.get(restConnectorForm).within(() => {
cy.get('#wallet').click().type(Cypress.env('vegaWalletName'));
cy.get('#passphrase').click().type(Cypress.env('vegaWalletPassphrase'));
cy.get('button').contains('Connect').click();
});
});
it('should have VEGA WALLET header visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletHeader)
.should('be.visible')
.and('have.text', 'Vega Wallet');
});
});
it('should have truncated account number visible', function () {
cy.get(walletContainer).within(() => {
cy.get(accountNo)
.should('be.visible')
.and('have.text', Cypress.env('vegaWalletPublicKeyShort'));
});
});
it('should have wallet name visible', function () {
cy.get(walletContainer).within(() => {
cy.get(walletName)
.should('be.visible')
.and('have.text', `${Cypress.env('vegaWalletName')} key 1`);
});
});
it('should have Vega Associated currency title visible', function () {
cy.get(walletContainer).within(() => {
cy.get(currencyTitle)
.should('be.visible')
.and('have.text', `VEGAAssociated`);
});
});
it('should have Vega Associated currency value visible', function () {
cy.get(walletContainer).within(() => {
cy.get(currencyValue)
.should('be.visible')
.and('have.text', `0.000000000000000000`);
});
});
it('should have Unstaked value visible', function () {
cy.get(walletContainer).within(() => {
cy.get(vegaUnstaked)
.should('be.visible')
.and('have.text', `0.000000000000000000`);
});
});
it('should have Governance button visible', function () {
cy.get(walletContainer).within(() => {
cy.get(governanceBtn)
.should('be.visible')
.and('have.text', 'Governance');
});
});
it('should have Staking button visible', function () {
cy.get(walletContainer).within(() => {
cy.get(stakingBtn).should('be.visible').and('have.text', 'Staking');
});
});
it('should have Manage link visible', function () {
cy.get(walletContainer).within(() => {
cy.get(manageLink).should('be.visible').and('have.text', 'Manage');
});
});
});
describe('when Manage dialog opened', function () {
before('click Manage link', function () {
cy.get(walletContainer).within(() => {
cy.get(manageLink).click();
});
});
it('should have SELECT A VEGA KEY dialog title visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogHeader)
.should('be.visible')
.and('have.text', 'SELECT A VEGA KEY');
});
});
it('should have wallet name visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogWalletName)
.should('be.visible')
.and('have.text', `${Cypress.env('vegaWalletName')} key 1`);
});
});
it('should have vega wallet public key visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogVegaKey)
.should('be.visible')
.and('have.text', `${Cypress.env('vegaWalletPublicKey')}`);
});
});
it('should have copy public key button visible', function () {
cy.get(dialog).within(() => {
cy.get(copyPublicKeyBtn).should('be.visible').and('have.text', 'Copy');
});
});
it('should have close button visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogCloseBtn).should('be.visible');
});
});
it('should have vega Disconnect all keys button visible', function () {
cy.get(dialog).within(() => {
cy.get(dialogDisconnectBtn)
.should('be.visible')
.and('have.text', 'Disconnect all keys');
});
});
it('should be able to disconnect all keys', function () {
cy.get(dialog).within(() => {
cy.get(dialogDisconnectBtn).click();
});
cy.get(walletContainer).within(() => {
cy.get(connectButton).should('be.visible');
});
});
});
});
@@ -75,7 +75,7 @@ Cypress.Commands.add(
(stakingBridgeContract) => {
cy.highlight('Tearing down staking tokens from vega wallet if present');
cy.wrap(
stakingBridgeContract.stakeBalance(ethWalletPubKey, vegaWalletPubKey),
stakingBridgeContract.stake_balance(ethWalletPubKey, vegaWalletPubKey),
{
timeout: transactionTimeout,
log: false,
@@ -83,7 +83,7 @@ Cypress.Commands.add(
).then((stake_amount) => {
if (String(stake_amount) != '0') {
cy.wrap(
stakingBridgeContract.removeStake(stake_amount, vegaWalletPubKey),
stakingBridgeContract.remove_stake(stake_amount, vegaWalletPubKey),
{ timeout: transactionTimeout, log: false }
).then((tx) => {
cy.wait_for_transaction(tx);
@@ -95,12 +95,12 @@ Cypress.Commands.add(
Cypress.Commands.add('vega_wallet_teardown_vesting', (vestingContract) => {
cy.highlight('Tearing down vesting tokens from vega wallet if present');
cy.wrap(vestingContract.stakeBalance(ethWalletPubKey, vegaWalletPubKey), {
cy.wrap(vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey), {
timeout: transactionTimeout,
log: false,
}).then((vesting_amount) => {
if (String(vesting_amount) != '0') {
cy.wrap(vestingContract.removeStake(vesting_amount, vegaWalletPubKey), {
cy.wrap(vestingContract.remove_stake(vesting_amount, vegaWalletPubKey), {
timeout: transactionTimeout,
log: false,
}).then((tx) => {
+1
View File
@@ -8,6 +8,7 @@ NX_FAIRGROUND=false
NX_IS_NEW_BRIDGE_CONTRACT=true
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET":"https://dev.token.vega.xyz","STAGNET2":"staging2.token.vega.xyz","TESTNET":"token.fairground.wtf","MAINNET":"token.vega.xyz"}'
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
#Test configuration variables
CYPRESS_FAIRGROUND=false
+1 -1
View File
@@ -49,7 +49,7 @@ const AppContainer = () => {
<AppLoader>
<BalanceManager>
<>
<div className="app dark max-w-[1300px] mx-auto my-0 grid grid-rows-[min-content_1fr_min-content] min-h-full lg:border-l-1 lg:border-r-1 lg:border-white font-sans text-body lg:text-body-large text-white-80">
<div className="app max-w-[1300px] mx-auto my-0 grid grid-rows-[min-content_1fr_min-content] min-h-full lg:border-l-1 lg:border-r-1 lg:border-white font-sans text-body lg:text-body-large text-white-80">
<AppBanner />
<TemplateSidebar sidebar={sideBar}>
<AppRouter />
@@ -25,7 +25,7 @@ export const SplashLoader = ({ text = 'Loading' }: { text?: string }) => {
);
})}
</div>
<div>{text}</div>
<div className="text-white">{text}</div>
</div>
);
};
@@ -18,6 +18,7 @@ import type {
} from './__generated__/Delegations';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useContracts } from '../../contexts/contracts/contracts-context';
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
const DELEGATIONS_QUERY = gql`
query Delegations($partyId: ID!) {
@@ -117,7 +118,7 @@ export const usePollForDelegations = () => {
.filter((a) => a.type === AccountType.General)
.map((a) => {
const isVega =
a.asset.source.__typename === 'ERC20' &&
isAssetTypeERC20(a.asset) &&
a.asset.source.contractAddress === vegaToken.address;
return {
@@ -131,10 +132,9 @@ export const usePollForDelegations = () => {
),
image: isVega ? vegaBlack : noIcon,
border: isVega,
address:
a.asset.source.__typename === 'ERC20'
? a.asset.source.contractAddress
: undefined,
address: isAssetTypeERC20(a.asset)
? a.asset.source.contractAddress
: undefined,
};
})
.sort((a, b) => {
@@ -43,10 +43,16 @@ export const VegaWallet = () => {
<h1 className="col-start-1 m-0">{t('vegaWallet')}</h1>
{keypair && (
<>
<div className="sm:row-start-2 sm:col-start-1 sm:col-span-2 text-h6 mb-12">
<div
data-testid="wallet-name"
className="sm:row-start-2 sm:col-start-1 sm:col-span-2 text-h6 mb-12"
>
{keypair.name}
</div>
<span className="sm:col-start-2 place-self-end font-mono pb-2 px-4">
<span
data-testid="vega-account-truncated"
className="sm:col-start-2 place-self-end font-mono pb-2 px-4"
>
{truncateMiddle(keypair.pub)}
</span>
</>
@@ -129,6 +135,7 @@ const VegaWalletConnected = ({ vegaKeys }: VegaWalletConnectedProps) => {
const footer = (
<WalletCardActions>
<Button
data-testid="manage-vega-wallet"
variant="inline-link"
className="mt-4"
onClick={() =>
+2 -6
View File
@@ -2,7 +2,6 @@ import React from 'react';
import { usePrevious } from './use-previous';
import type { BigNumber } from '../lib/bignumber';
import { theme as tailwindcss } from '@vegaprotocol/tailwindcss-config';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
const Colors = tailwindcss.colors;
const FLASH_DURATION = 1200; // Duration of flash animation in milliseconds
@@ -13,7 +12,6 @@ export function useAnimateValue(
) {
const shouldAnimate = React.useRef(false);
const previous = usePrevious(value);
const [theme] = useThemeSwitcher();
React.useEffect(() => {
const timeout = setTimeout(() => {
@@ -38,8 +36,7 @@ export function useAnimateValue(
offset: 0.8,
},
{
backgroundColor:
theme === 'dark' ? Colors.white[60] : Colors.black[60],
backgroundColor: Colors.white[60],
color: Colors.white.DEFAULT,
},
],
@@ -64,8 +61,7 @@ export function useAnimateValue(
offset: 0.8,
},
{
backgroundColor:
theme === 'dark' ? Colors.white[60] : Colors.black[60],
backgroundColor: Colors.white[60],
color: Colors.white.DEFAULT,
},
],
@@ -189,7 +189,6 @@ export const ProposalsListItemDetails = ({
className="col-start-2 row-start-1 justify-self-end"
data-testid="vote-status"
>
testing testing 123
{voteStatus}
</div>
)}
@@ -1,2 +1 @@
export { VoteDetails } from './vote-details';
export { VOTE_VALUE_MAP } from './vote-types';
@@ -2,8 +2,7 @@ import { captureException, captureMessage } from '@sentry/minimal';
import * as React from 'react';
import { VoteValue } from '../../../../__generated__/globalTypes';
import { VOTE_VALUE_MAP } from './vote-types';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useVegaWallet, VegaWalletVoteValue } from '@vegaprotocol/wallet';
export type Vote = {
value: VoteValue;
@@ -101,7 +100,7 @@ export function useUserVote(
pubKey: keypair.pub,
propagate: true,
voteSubmission: {
value: VOTE_VALUE_MAP[value],
value: VegaWalletVoteValue[value],
proposalId,
},
};
@@ -1,6 +0,0 @@
import { VoteValue } from '../../../../__generated__/globalTypes';
export const VOTE_VALUE_MAP = {
[VoteValue.Yes]: 'VALUE_YES',
[VoteValue.No]: 'VALUE_NO',
} as const;
@@ -63,7 +63,7 @@ export const VestingTable = ({
{formatNumber(associated)}
</KeyValueTableRow>
</KeyValueTable>
<div className="flex">
<div className="flex border-white border">
<div
className="bg-vega-pink h-16"
style={{ flex: lockedPercentage.toNumber() }}
@@ -3,6 +3,8 @@
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: Rewards
// ====================================================
@@ -45,6 +47,10 @@ export interface Rewards_party_rewardDetails_rewards_epoch {
export interface Rewards_party_rewardDetails_rewards {
__typename: "Reward";
/**
* The type of reward
*/
rewardType: AccountType;
/**
* The asset this reward is paid in
*/
@@ -30,6 +30,7 @@ export const REWARDS_QUERY = gql`
symbol
}
rewards {
rewardType
asset {
id
}
@@ -19,10 +19,6 @@ interface RewardInfoProps {
rewardAssetId: string;
}
// Note: For now the only reward type is Staking. We'll need this from the API
// at a later date
const DEFAULT_REWARD_TYPE = 'Staking';
export const RewardInfo = ({
data,
currVegaKey,
@@ -119,7 +115,7 @@ export const RewardTable = ({ reward, delegations }: RewardTableProps) => {
<KeyValueTable>
<KeyValueTableRow>
{t('rewardType')}
<span>{DEFAULT_REWARD_TYPE}</span>
<span>{reward.rewardType}</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('yourStake')}
+31 -8
View File
@@ -57,6 +57,8 @@ interface ValidatorRendererProps {
data: { validator: { avatarUrl: string; name: string } };
}
const stripNonDigits = (string: string) => string.replace(/\D/g, '');
const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
const { avatarUrl, name } = data.validator;
return (
@@ -66,6 +68,7 @@ const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
className="h-24 w-24 rounded-full mr-8"
src={avatarUrl}
alt={`Avatar icon for ${name}`}
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
)}
{name}
@@ -142,7 +145,7 @@ export const NodeList = ({ epoch }: NodeListProps) => {
[TOTAL_STAKE_THIS_EPOCH]: formatNumber(stakedTotal, 2),
[SHARE]: stakedTotalPercentage,
[VALIDATOR_STAKE]: formatNumber(stakedOnNode, 2),
[PENDING_STAKE]: pendingStake,
[PENDING_STAKE]: formatNumber(pendingStake, 2),
[RANKING_SCORE]: formatNumber(new BigNumber(rankingScore), 5),
[STAKE_SCORE]: formatNumber(new BigNumber(stakeScore), 5),
[PERFORMANCE_SCORE]: formatNumber(new BigNumber(performanceScore), 5),
@@ -167,16 +170,34 @@ export const NodeList = ({ epoch }: NodeListProps) => {
field: TOTAL_STAKE_THIS_EPOCH,
headerName: t('totalStakeThisEpoch').toString(),
},
{ field: SHARE, headerName: t('share').toString() },
{ field: VALIDATOR_STAKE, headerName: t('validatorStake').toString() },
{ field: PENDING_STAKE, headerName: t('nextEpoch').toString() },
{ field: RANKING_SCORE, headerName: t('rankingScore').toString() },
{ field: STAKE_SCORE, headerName: t('stakeScore').toString() },
{
field: SHARE,
headerName: t('share').toString(),
},
{
field: VALIDATOR_STAKE,
headerName: t('validatorStake').toString(),
},
{
field: PENDING_STAKE,
headerName: t('nextEpoch').toString(),
},
{
field: RANKING_SCORE,
headerName: t('rankingScore').toString(),
},
{
field: STAKE_SCORE,
headerName: t('stakeScore').toString(),
},
{
field: PERFORMANCE_SCORE,
headerName: t('performanceScore').toString(),
},
{ field: VOTING_POWER, headerName: t('votingPower').toString() },
{
field: VOTING_POWER,
headerName: t('votingPower').toString(),
},
],
[]
);
@@ -184,6 +205,8 @@ export const NodeList = ({ epoch }: NodeListProps) => {
const defaultColDef = useMemo(
() => ({
sortable: true,
comparator: (a: string, b: string) =>
parseFloat(stripNonDigits(a)) - parseFloat(stripNonDigits(b)),
}),
[]
);
@@ -206,7 +229,7 @@ export const NodeList = ({ epoch }: NodeListProps) => {
event.columnApi.applyColumnState({
state: [
{
colId: t('rankingScore'),
colId: RANKING_SCORE,
sort: 'desc',
},
],
@@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { BigNumber } from '../../lib/bignumber';
import { removeDecimal } from '../../lib/decimals';
import type { UndelegateSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { UndelegateSubmissionBody } from '@vegaprotocol/vegawallet-service-api-client';
interface PendingStakeProps {
pendingAmount: BigNumber;
@@ -26,11 +26,11 @@ import {
Radio,
RadioGroup,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type {
DelegateSubmissionBody,
UndelegateSubmissionBody,
} from '@vegaprotocol/vegawallet-service-api-client';
} from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
export const PARTY_DELEGATIONS_QUERY = gql`
query PartyDelegations($partyId: ID!) {
+1
View File
@@ -1 +1,2 @@
NX_USE_ENV_OVERRIDES=0
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
@@ -29,7 +29,7 @@ describe('vega wallet', () => {
cy.getByTestId(form).find('#wallet').click().type('invalid name');
cy.getByTestId(form).find('#passphrase').click().type('invalid password');
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
cy.getByTestId('form-error').should('have.text', 'Authentication failed');
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
});
it('doesnt connect with invalid fields', () => {
+1
View File
@@ -6,3 +6,4 @@ NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET\":\"https://alpha.console.vega.xyz\"}
NX_USE_ENV_OVERRIDES=1
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789/api/v1
@@ -63,7 +63,10 @@ export const Web3Content = ({
if (connector?.connectEagerly && !('Cypress' in window)) {
connector.connectEagerly();
}
}, [connector]);
// wallet connect doesnt handle connectEagerly being called when connector is also in the
// deps array.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (error) {
return (
+6 -2
View File
@@ -28,7 +28,7 @@ function AppBody({ Component, pageProps }: AppProps) {
return (
<ThemeContext.Provider value={theme}>
<div className="h-full text-white relative text-black-60 dark:text-white-60 z-0 grid grid-rows-[min-content,1fr]">
<div className="h-full relative text-black-60 dark:text-white-60 z-0 grid grid-rows-[min-content,1fr]">
<AppLoader>
<div className="flex items-stretch border-b-[7px] bg-black border-vega-pink dark:border-vega-yellow">
<Navbar />
@@ -41,7 +41,11 @@ function AppBody({ Component, pageProps }: AppProps) {
store.setVegaWalletManageDialog(open);
}}
/>
<ThemeSwitcher onToggle={toggleTheme} className="-my-4" />
<ThemeSwitcher
onToggle={toggleTheme}
className="-my-4"
sunClassName="text-white"
/>
</div>
</div>
<main data-testid={pageProps.page} className="dark:bg-black">
@@ -16,14 +16,10 @@ const DEPOSIT_PAGE_QUERY = gql`
}
`;
interface DepositContainerProps {
assetId?: string;
}
/**
* Fetches data required for the Deposit page
*/
export const DepositContainer = ({ assetId }: DepositContainerProps) => {
export const DepositContainer = () => {
const { VEGA_ENV } = useEnvironment();
return (
@@ -41,7 +37,6 @@ export const DepositContainer = ({ assetId }: DepositContainerProps) => {
return (
<DepositManager
assets={data.assets}
initialAssetId={assetId}
isFaucetable={VEGA_ENV !== 'MAINNET'}
/>
);
@@ -1,29 +1,12 @@
import { useRouter } from 'next/router';
import { useMemo } from 'react';
import { Web3Container } from '../../../components/web3-container';
import { DepositContainer } from './deposit-container';
const Deposit = () => {
const { query } = useRouter();
// AssetId can be specified in the query string to allow link to deposit a particular asset
const assetId = useMemo(() => {
if (query.assetId && Array.isArray(query.assetId)) {
return undefined;
}
if (Array.isArray(query.assetId)) {
return undefined;
}
return query.assetId;
}, [query]);
return (
<Web3Container>
<div className="max-w-[420px] p-24 mx-auto">
<h1 className="text-h3 mb-12">Deposit</h1>
<DepositContainer assetId={assetId} />
<DepositContainer />
</div>
</Web3Container>
);
@@ -1,5 +1,5 @@
import merge from 'lodash/merge';
import type { TransactionResponse } from '@vegaprotocol/vegawallet-service-api-client';
import type { TransactionResponse } from '@vegaprotocol/wallet';
import type { PartialDeep } from 'type-fest';
declare global {
@@ -17,7 +17,10 @@ export function addMockVegaWalletCommands() {
'mockVegaCommandSync',
(override?: PartialDeep<TransactionResponse>) => {
const defaultTransactionResponse = {
txId: 'tx-id',
txHash: 'tx-hash',
sentAt: new Date().toISOString(),
receivedAt: new Date().toISOString(),
tx: {
input_data:
'CPe6vpiqsPqxDBDC1w7KPkoKQGE4Y2M0NjUwMjhiMGY4OTM4YTYzZTEzNDViYzM2ODc3ZWRmODg4MjNmOWU0ZmI4ZDRlN2VkMmFlMzAwNzA3ZTMYASABKAM4Ag==',
@@ -28,7 +31,7 @@ export function addMockVegaWalletCommands() {
version: 1,
},
From: {
PubKey: Cypress.env('vegaPublicKey'),
PubKey: Cypress.env('VEGA_PUBLIC_KEY'),
},
version: 2,
pow: {
@@ -44,7 +44,7 @@ export const DealTicketManager = ({
);
};
const getDialogTitle = (status?: OrderStatus): string | undefined => {
export const getDialogTitle = (status?: OrderStatus): string | undefined => {
if (!status) {
return;
}
@@ -63,7 +63,7 @@ const getDialogTitle = (status?: OrderStatus): string | undefined => {
}
};
const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
export const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
if (!status) {
return;
}
@@ -81,7 +81,7 @@ const getDialogIntent = (status?: OrderStatus): Intent | undefined => {
}
};
const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => {
export const getDialogIcon = (status?: OrderStatus): ReactNode | undefined => {
if (!status) {
return;
}
@@ -7,8 +7,8 @@ import { addDecimal } from '@vegaprotocol/react-helpers';
import { fireEvent, render, screen, act } from '@testing-library/react';
import { DealTicket } from './deal-ticket';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import type { Order } from '../utils/get-default-order';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { Order } from '@vegaprotocol/orders';
const market: DealTicketQuery_market = {
__typename: 'Market',
@@ -59,109 +59,111 @@ function generateJsx(order?: Order) {
);
}
it('Displays ticket defaults', () => {
render(generateJsx());
describe('DealTicket', () => {
it('Displays ticket defaults', () => {
render(generateJsx());
// Assert defaults are used
expect(
screen.getByTestId(`order-type-${VegaWalletOrderType.Market}-selected`)
).toBeInTheDocument();
expect(
screen.queryByTestId('order-side-SIDE_BUY-selected')
).toBeInTheDocument();
expect(
screen.queryByTestId('order-side-SIDE_SELL-selected')
).not.toBeInTheDocument();
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
String(1 / Math.pow(10, market.positionDecimalPlaces))
);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Assert defaults are used
expect(
screen.getByTestId(`order-type-${VegaWalletOrderType.Market}-selected`)
).toBeInTheDocument();
expect(
screen.queryByTestId('order-side-SIDE_BUY-selected')
).toBeInTheDocument();
expect(
screen.queryByTestId('order-side-SIDE_SELL-selected')
).not.toBeInTheDocument();
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
String(1 / Math.pow(10, market.positionDecimalPlaces))
);
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Assert last price is shown
expect(screen.getByTestId('last-price')).toHaveTextContent(
// eslint-disable-next-line
`~${addDecimal(market.depth.lastTrade!.price, market.decimalPlaces)} ${
market.tradableInstrument.instrument.product.quoteName
}`
);
});
// Assert last price is shown
expect(screen.getByTestId('last-price')).toHaveTextContent(
// eslint-disable-next-line
`~${addDecimal(market.depth.lastTrade!.price, market.decimalPlaces)} ${
market.tradableInstrument.instrument.product.quoteName
}`
);
});
it('Can edit deal ticket', async () => {
render(generateJsx());
it('Can edit deal ticket', async () => {
render(generateJsx());
// BUY is selected by default
screen.getByTestId('order-side-SIDE_BUY-selected');
// BUY is selected by default
screen.getByTestId('order-side-SIDE_BUY-selected');
await act(async () => {
fireEvent.change(screen.getByTestId('order-size'), {
target: { value: '200' },
await act(async () => {
fireEvent.change(screen.getByTestId('order-size'), {
target: { value: '200' },
});
});
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: VegaWalletOrderTimeInForce.IOC },
});
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Switch to limit order
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
// Assert price input shown with default value
expect(screen.getByTestId('order-price')).toHaveDisplayValue('0');
// Check all TIF options shown
expect(screen.getByTestId('order-tif').children).toHaveLength(
Object.keys(VegaWalletOrderTimeInForce).length
);
});
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
it('Handles TIF select box dependent on order type', () => {
render(generateJsx());
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: VegaWalletOrderTimeInForce.IOC },
// Check only IOC and
expect(
Array.from(screen.getByTestId('order-tif').children).map(
(o) => o.textContent
)
).toEqual(['Immediate or Cancel (IOC)', 'Fill or Kill (FOK)']);
// Switch to limit order and check all TIF options shown
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
expect(screen.getByTestId('order-tif').children).toHaveLength(
Object.keys(VegaWalletOrderTimeInForce).length
);
// Change to GTC
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: VegaWalletOrderTimeInForce.GTC },
});
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.GTC
);
// Switch back to market order and TIF should now be IOC
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Switch tif to FOK
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: VegaWalletOrderTimeInForce.FOK },
});
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.FOK
);
// Change back to limit and check we are still on FOK
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.FOK
);
});
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Switch to limit order
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
// Assert price input shown with default value
expect(screen.getByTestId('order-price')).toHaveDisplayValue('0');
// Check all TIF options shown
expect(screen.getByTestId('order-tif').children).toHaveLength(
Object.keys(VegaWalletOrderTimeInForce).length
);
});
it('Handles TIF select box dependent on order type', () => {
render(generateJsx());
// Check only IOC and
expect(
Array.from(screen.getByTestId('order-tif').children).map(
(o) => o.textContent
)
).toEqual(['IOC', 'FOK']);
// Switch to limit order and check all TIF options shown
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
expect(screen.getByTestId('order-tif').children).toHaveLength(
Object.keys(VegaWalletOrderTimeInForce).length
);
// Change to GTC
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: VegaWalletOrderTimeInForce.GTC },
});
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.GTC
);
// Switch back to market order and TIF should now be IOC
fireEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.IOC
);
// Switch tif to FOK
fireEvent.change(screen.getByTestId('order-tif'), {
target: { value: VegaWalletOrderTimeInForce.FOK },
});
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.FOK
);
// Change back to limit and check we are still on FOK
fireEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
expect(screen.getByTestId('order-tif')).toHaveValue(
VegaWalletOrderTimeInForce.FOK
);
});
@@ -11,6 +11,26 @@ interface TimeInForceSelectorProps {
onSelect: (tif: VegaWalletOrderTimeInForce) => void;
}
// More detail in https://docs.vega.xyz/docs/mainnet/graphql/enums/order-time-in-force
export const timeInForceLabel = (tif: string) => {
switch (tif) {
case VegaWalletOrderTimeInForce.GTC:
return t(`Good 'til Cancelled`);
case VegaWalletOrderTimeInForce.IOC:
return t('Immediate or Cancel');
case VegaWalletOrderTimeInForce.FOK:
return t('Fill or Kill');
case VegaWalletOrderTimeInForce.GTT:
return t(`Good 'til Time`);
case VegaWalletOrderTimeInForce.GFN:
return t('Good for Normal');
case VegaWalletOrderTimeInForce.GFA:
return t('Good for Auction');
default:
return t(tif);
}
};
export const TimeInForceSelector = ({
value,
orderType,
@@ -37,7 +57,7 @@ export const TimeInForceSelector = ({
{options.map(([key, value]) => {
return (
<option key={key} value={value}>
{key}
{`${timeInForceLabel(value)} (${key})`}
</option>
);
})}
+12 -15
View File
@@ -37,10 +37,8 @@ beforeEach(() => {
submitApprove: jest.fn(),
submitDeposit: jest.fn(),
requestFaucet: jest.fn(),
limits: {
max: new BigNumber(20),
deposited: new BigNumber(10),
},
max: new BigNumber(20),
deposited: new BigNumber(10),
allowance: new BigNumber(30),
isFaucetable: true,
};
@@ -134,7 +132,8 @@ describe('Deposit form', () => {
<DepositForm
{...props}
balance={new BigNumber(100)}
limits={{ max: new BigNumber(100), deposited: new BigNumber(10) }}
max={new BigNumber(100)}
deposited={new BigNumber(10)}
/>
);
@@ -213,18 +212,16 @@ describe('Deposit form', () => {
const mockUseWeb3React = useWeb3React as jest.Mock;
mockUseWeb3React.mockReturnValue({ account });
const limits = {
max: new BigNumber(20),
deposited: new BigNumber(10),
};
const balance = new BigNumber(50);
const max = new BigNumber(20);
const deposited = new BigNumber(10);
render(
<DepositForm
{...props}
allowance={new BigNumber(100)}
balance={balance}
limits={limits}
max={max}
deposited={deposited}
selectedAsset={asset}
/>
);
@@ -237,13 +234,13 @@ describe('Deposit form', () => {
expect(
screen.getByText('Maximum total deposit amount', { selector: 'th' })
.nextElementSibling
).toHaveTextContent(limits.max.toString());
).toHaveTextContent(max.toString());
expect(
screen.getByText('Deposited', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.deposited.toString());
).toHaveTextContent(deposited.toString());
expect(
screen.getByText('Remaining', { selector: 'th' }).nextElementSibling
).toHaveTextContent(limits.max.minus(limits.deposited).toString());
).toHaveTextContent(max.minus(deposited).toString());
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '8' },
@@ -257,7 +254,7 @@ describe('Deposit form', () => {
expect(props.submitDeposit).toHaveBeenCalledWith({
// @ts-ignore contract address definitely defined
assetSource: asset.source.contractAddress,
amount: '800',
amount: '8',
vegaPublicKey: vegaKey,
});
});
+41 -35
View File
@@ -1,5 +1,5 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import {
removeDecimal,
ethereumAddress,
t,
required,
@@ -7,6 +7,7 @@ import {
minSafe,
maxSafe,
addDecimal,
isAssetTypeERC20,
} from '@vegaprotocol/react-helpers';
import {
Button,
@@ -21,10 +22,9 @@ import { useWeb3React } from '@web3-react/core';
import { Web3WalletInput } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react';
import { useMemo, useEffect } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useMemo } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import type { Asset } from './deposit-manager';
interface FormFields {
asset: string;
@@ -45,10 +45,8 @@ export interface DepositFormProps {
vegaPublicKey: string;
}) => void;
requestFaucet: () => void;
limits: {
max: BigNumber;
deposited: BigNumber;
} | null;
max: BigNumber | undefined;
deposited: BigNumber | undefined;
allowance: BigNumber | undefined;
isFaucetable?: boolean;
}
@@ -58,10 +56,11 @@ export const DepositForm = ({
selectedAsset,
onSelectAsset,
balance,
max,
deposited,
submitApprove,
submitDeposit,
requestFaucet,
limits,
allowance,
isFaucetable,
}: DepositFormProps) => {
@@ -89,15 +88,14 @@ export const DepositForm = ({
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: removeDecimal(fields.amount, selectedAsset.decimals),
amount: fields.amount,
vegaPublicKey: fields.to,
});
};
const assetId = useWatch({ name: 'asset', control });
const amount = useWatch({ name: 'amount', control });
const max = useMemo(() => {
const maxAmount = useMemo(() => {
const maxApproved = allowance ? allowance : new BigNumber(0);
const maxAvailable = balance ? balance : new BigNumber(0);
@@ -106,8 +104,8 @@ export const DepositForm = ({
let maxLimit = new BigNumber(Infinity);
// A max limit of zero indicates that there is no limit
if (limits && limits.max.isGreaterThan(0)) {
maxLimit = limits.max.minus(limits.deposited);
if (max && deposited && max.isGreaterThan(0)) {
maxLimit = max.minus(deposited);
}
return {
@@ -116,7 +114,7 @@ export const DepositForm = ({
limit: maxLimit,
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
};
}, [limits, allowance, balance]);
}, [max, deposited, allowance, balance]);
const min = useMemo(() => {
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
@@ -127,10 +125,6 @@ export const DepositForm = ({
return minViableAmount;
}, [selectedAsset]);
useEffect(() => {
onSelectAsset(assetId);
}, [assetId, onSelectAsset]);
return (
<form
onSubmit={handleSubmit(onDeposit)}
@@ -154,16 +148,28 @@ export const DepositForm = ({
)}
</FormGroup>
<FormGroup label={t('Asset')} labelFor="asset" className="relative">
<Select {...register('asset', { validate: { required } })} id="asset">
<option value="">{t('Please select')}</option>
{assets
.filter((a) => a.source.__typename === 'ERC20')
.map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</Select>
<Controller
control={control}
name="asset"
rules={{ validate: { required } }}
render={({ field }) => (
<Select
id="asset"
{...field}
onChange={(e) => {
field.onChange(e);
onSelectAsset(e.target.value);
}}
>
<option value="">{t('Please select')}</option>
{assets.filter(isAssetTypeERC20).map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</Select>
)}
/>
{errors.asset?.message && (
<InputError intent="danger" className="mt-4" forInput="asset">
{errors.asset.message}
@@ -196,9 +202,9 @@ export const DepositForm = ({
</UseButton>
)}
</FormGroup>
{selectedAsset && limits && (
{selectedAsset && max && deposited && (
<div className="mb-20">
<DepositLimits limits={limits} balance={balance} />
<DepositLimits max={max} deposited={deposited} balance={balance} />
</div>
)}
<FormGroup label={t('Amount')} labelFor="amount" className="relative">
@@ -212,14 +218,14 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max.available)) {
if (value.isGreaterThan(maxAmount.available)) {
return t('Insufficient amount in Ethereum wallet');
} else if (value.isGreaterThan(max.limit)) {
} else if (value.isGreaterThan(maxAmount.limit)) {
return t('Amount is above temporary deposit limit');
} else if (value.isGreaterThan(max.approved)) {
} else if (value.isGreaterThan(maxAmount.approved)) {
return t('Amount is above approved amount');
}
return maxSafe(max.amount)(v);
return maxSafe(maxAmount.amount)(v);
},
},
})}
+13 -11
View File
@@ -2,28 +2,30 @@ import { t } from '@vegaprotocol/react-helpers';
import type BigNumber from 'bignumber.js';
interface DepositLimitsProps {
limits: {
max: BigNumber;
deposited: BigNumber;
};
max: BigNumber;
deposited: BigNumber;
balance?: BigNumber;
}
export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => {
export const DepositLimits = ({
max,
deposited,
balance,
}: DepositLimitsProps) => {
let maxLimit = '';
if (limits.max.isEqualTo(Infinity)) {
if (max.isEqualTo(Infinity)) {
maxLimit = t('No limit');
} else if (limits.max.isGreaterThan(1_000_000)) {
} else if (max.isGreaterThan(1_000_000)) {
maxLimit = t('1m+');
} else {
maxLimit = limits.max.toString();
maxLimit = max.toString();
}
let remaining = '';
if (limits.deposited.isEqualTo(0)) {
if (deposited.isEqualTo(0)) {
remaining = maxLimit;
} else {
const amountRemaining = limits.max.minus(limits.deposited);
const amountRemaining = max.minus(deposited);
remaining = amountRemaining.isGreaterThan(1_000_000)
? t('1m+')
: amountRemaining.toString();
@@ -44,7 +46,7 @@ export const DepositLimits = ({ limits, balance }: DepositLimitsProps) => {
</tr>
<tr>
<th className="text-left font-normal">{t('Deposited')}</th>
<td className="text-right">{limits.deposited.toString()}</td>
<td className="text-right">{deposited.toString()}</td>
</tr>
<tr>
<th className="text-left font-normal">{t('Remaining')}</th>
+23 -88
View File
@@ -1,121 +1,56 @@
import { useEffect, useMemo, useState } from 'react';
import { DepositForm } from './deposit-form';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { useSubmitDeposit } from './use-submit-deposit';
import sortBy from 'lodash/sortBy';
import { useSubmitApproval } from './use-submit-approval';
import { useGetDepositLimits } from './use-get-deposit-limits';
import { useGetAllowance } from './use-get-allowance';
import { useSubmitFaucet } from './use-submit-faucet';
import { EthTxStatus, useEthereumConfig } from '@vegaprotocol/web3';
import { useTokenContract } from '@vegaprotocol/web3';
import { removeDecimal } from '@vegaprotocol/react-helpers';
interface ERC20AssetSource {
__typename: 'ERC20';
contractAddress: string;
}
interface BuiltinAssetSource {
__typename: 'BuiltinAsset';
}
type AssetSource = ERC20AssetSource | BuiltinAssetSource;
export interface Asset {
__typename: 'Asset';
id: string;
symbol: string;
name: string;
decimals: number;
source: AssetSource;
}
import { useDepositStore } from './deposit-store';
import { useCallback } from 'react';
import { useDepositBalances } from './use-deposit-balances';
import type { Asset } from '@vegaprotocol/react-helpers';
interface DepositManagerProps {
assets: Asset[];
initialAssetId?: string;
isFaucetable?: boolean;
isFaucetable: boolean;
}
export const DepositManager = ({
assets,
initialAssetId,
isFaucetable,
}: DepositManagerProps) => {
const [assetId, setAssetId] = useState<string | undefined>(initialAssetId);
// Find the asset object from the select box
const asset = useMemo(() => {
const asset = assets?.find((a) => a.id === assetId);
return asset;
}, [assets, assetId]);
const { config } = useEthereumConfig();
const tokenContract = useTokenContract(
asset?.source.__typename === 'ERC20'
? asset.source.contractAddress
: undefined,
isFaucetable
);
// Get users balance of the erc20 token selected
const { balance, refetch: refetchBalance } = useGetBalanceOfERC20Token(
tokenContract,
asset?.decimals
);
// Get temporary deposit limits
const limits = useGetDepositLimits(asset);
// Get allowance (approved spending limit of brdige contract) for the selected asset
const { allowance, refetch: refetchAllowance } = useGetAllowance(
tokenContract,
asset?.decimals
);
const { asset, balance, allowance, deposited, max, update } =
useDepositStore();
useDepositBalances(isFaucetable);
// Set up approve transaction
const approve = useSubmitApproval(tokenContract);
const approve = useSubmitApproval();
// Set up deposit transaction
const deposit = useSubmitDeposit();
// Set up faucet transaction
const faucet = useSubmitFaucet(tokenContract);
const faucet = useSubmitFaucet();
// Update balance after confirmation event has been received
useEffect(() => {
if (
faucet.transaction.status === EthTxStatus.Confirmed ||
deposit.transaction.status === EthTxStatus.Confirmed
) {
refetchBalance();
}
}, [deposit.transaction.status, faucet.transaction.status, refetchBalance]);
// After an approval transaction refetch allowance
useEffect(() => {
if (approve.transaction.status === EthTxStatus.Confirmed) {
refetchAllowance();
}
}, [approve.transaction.status, refetchAllowance]);
const handleSelectAsset = useCallback(
(id) => {
const asset = assets.find((a) => a.id === id);
if (!asset) return;
update({ asset });
},
[assets, update]
);
return (
<>
<DepositForm
balance={balance}
selectedAsset={asset}
onSelectAsset={(id) => setAssetId(id)}
onSelectAsset={handleSelectAsset}
assets={sortBy(assets, 'name')}
submitApprove={() => {
if (!asset || !config) return;
const amount = removeDecimal('1000000', asset.decimals);
approve.perform(config.collateral_bridge_contract.address, amount);
}}
submitDeposit={(args) => {
deposit.perform(args.assetSource, args.amount, args.vegaPublicKey);
}}
submitApprove={() => approve.perform()}
submitDeposit={(args) => deposit.perform(args)}
requestFaucet={() => faucet.perform()}
limits={limits}
deposited={deposited}
max={max}
allowance={allowance}
isFaucetable={isFaucetable}
/>
+24
View File
@@ -0,0 +1,24 @@
import type { Asset } from '@vegaprotocol/react-helpers';
import BigNumber from 'bignumber.js';
import type { SetState } from 'zustand';
import create from 'zustand';
interface DepositStore {
balance: BigNumber;
allowance: BigNumber;
asset: Asset | undefined;
deposited: BigNumber;
max: BigNumber;
update: (state: Partial<DepositStore>) => void;
}
export const useDepositStore = create((set: SetState<DepositStore>) => ({
balance: new BigNumber(0),
allowance: new BigNumber(0),
deposited: new BigNumber(0),
max: new BigNumber(0),
asset: undefined,
update: (updatedState) => {
set(updatedState);
},
}));
@@ -0,0 +1,59 @@
import { useBridgeContract, useTokenContract } from '@vegaprotocol/web3';
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { useDepositStore } from './deposit-store';
import { useGetAllowance } from './use-get-allowance';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { useGetDepositMaximum } from './use-get-deposit-maximum';
import { useGetDepositedAmount } from './use-get-deposited-amount';
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
/**
* Hook which fetches all the balances required for despoiting
* whenever the asset changes in the form
*/
export const useDepositBalances = (isFaucetable: boolean) => {
const { asset, update } = useDepositStore();
const tokenContract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
isFaucetable
);
const bridgeContract = useBridgeContract(true);
const getAllowance = useGetAllowance(tokenContract, asset);
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
const getDepositedAmount = useGetDepositedAmount(asset);
useEffect(() => {
const getBalances = async () => {
try {
const [max, deposited, balance, allowance] = await Promise.all([
getDepositMaximum(),
getDepositedAmount(),
getBalance(),
getAllowance(),
]);
update({
max,
deposited,
balance,
allowance,
});
} catch (err) {
Sentry.captureException(err);
}
};
if (asset) {
getBalances();
}
}, [
asset,
update,
getDepositMaximum,
getDepositedAmount,
getAllowance,
getBalance,
]);
};
+21 -16
View File
@@ -1,30 +1,35 @@
import type { Token } from '@vegaprotocol/smart-contracts';
import * as Sentry from '@sentry/react';
import { useWeb3React } from '@web3-react/core';
import { useCallback } from 'react';
import { useEthereumConfig, useEthereumReadContract } from '@vegaprotocol/web3';
import { useEthereumConfig } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers';
export const useGetAllowance = (contract: Token | null, decimals?: number) => {
export const useGetAllowance = (
contract: Token | null,
asset: Asset | undefined
) => {
const { account } = useWeb3React();
const { config } = useEthereumConfig();
const getAllowance = useCallback(() => {
if (!contract || !account || !config) {
const getAllowance = useCallback(async () => {
if (!contract || !account || !config || !asset) {
return;
}
return contract.allowance(
account,
config.collateral_bridge_contract.address
);
}, [contract, account, config]);
try {
const res = await contract.allowance(
account,
config.collateral_bridge_contract.address
);
const { state, refetch } = useEthereumReadContract(getAllowance);
return new BigNumber(addDecimal(res.toString(), asset.decimals));
} catch (err) {
Sentry.captureException(err);
return;
}
}, [contract, account, config, asset]);
const allowance =
state.data && decimals
? new BigNumber(addDecimal(state.data.toString(), decimals))
: undefined;
return { allowance, refetch };
return getAllowance;
};
@@ -1,30 +1,29 @@
import { useEthereumReadContract } from '@vegaprotocol/web3';
import type { Token } from '@vegaprotocol/smart-contracts';
import * as Sentry from '@sentry/react';
import { useWeb3React } from '@web3-react/core';
import { useCallback } from 'react';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers';
export const useGetBalanceOfERC20Token = (
contract: Token | null,
decimals: number | undefined
asset: Asset | undefined
) => {
const { account } = useWeb3React();
const getBalance = useCallback(() => {
if (!contract || !account) {
const getBalance = useCallback(async () => {
if (!contract || !asset || !account) {
return;
}
return contract.balanceOf(account);
}, [contract, account]);
try {
const res = await contract.balanceOf(account);
return new BigNumber(addDecimal(res.toString(), asset.decimals));
} catch (err) {
Sentry.captureException(err);
return;
}
}, [contract, asset, account]);
const { state, refetch } = useEthereumReadContract(getBalance);
const balance =
state.data && decimals
? new BigNumber(addDecimal(state.data?.toString(), decimals))
: undefined;
return { balance, refetch };
return getBalance;
};
@@ -1,69 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { ethers } from 'ethers';
import type { Asset } from './deposit-manager';
import {
useBridgeContract,
useEthereumConfig,
useEthereumReadContract,
} from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { useWeb3React } from '@web3-react/core';
export const useGetDepositLimits = (asset?: Asset) => {
const { account, provider } = useWeb3React();
const { config } = useEthereumConfig();
const contract = useBridgeContract(true);
const [userTotal, setUserTotal] = useState<BigNumber | null>(null);
const getLimits = useCallback(async () => {
if (!contract || !asset || asset.source.__typename !== 'ERC20') {
return;
}
return contract.get_deposit_maximum(asset.source.contractAddress);
}, [asset, contract]);
useEffect(() => {
if (
!provider ||
!config ||
!account ||
!asset ||
asset.source.__typename !== 'ERC20'
) {
return;
}
const abicoder = new ethers.utils.AbiCoder();
const innerHash = ethers.utils.keccak256(
abicoder.encode(['address', 'uint256'], [account, 4])
);
const storageLocation = ethers.utils.keccak256(
abicoder.encode(
['address', 'bytes32'],
[asset.source.contractAddress, innerHash]
)
);
(async () => {
const res = await provider.getStorageAt(
config.collateral_bridge_contract.address,
storageLocation
);
const value = new BigNumber(res, 16).toString();
setUserTotal(new BigNumber(addDecimal(value, asset.decimals)));
})();
}, [provider, config, account, asset]);
const {
state: { data },
} = useEthereumReadContract(getLimits);
if (!data || !userTotal || !asset) return null;
const max = new BigNumber(addDecimal(data.toString(), asset.decimals));
return {
max: max.isEqualTo(0) ? new BigNumber(Infinity) : max,
deposited: userTotal,
};
};
@@ -0,0 +1,32 @@
import { useCallback } from 'react';
import * as Sentry from '@sentry/react';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers';
import type {
CollateralBridge,
CollateralBridgeNew,
} from '@vegaprotocol/smart-contracts';
export const useGetDepositMaximum = (
contract: CollateralBridge | CollateralBridgeNew | null,
asset: Asset | undefined
) => {
const getDepositMaximum = useCallback(async () => {
if (!contract || !asset || asset.source.__typename !== 'ERC20') {
return;
}
try {
const res = await contract.get_deposit_maximum(
asset.source.contractAddress
);
const max = new BigNumber(addDecimal(res.toString(), asset.decimals));
return max.isEqualTo(0) ? new BigNumber(Infinity) : max;
} catch (err) {
Sentry.captureException(err);
return;
}
}, [contract, asset]);
return getDepositMaximum;
};
@@ -0,0 +1,50 @@
import { useCallback } from 'react';
import * as Sentry from '@sentry/react';
import { ethers } from 'ethers';
import { useEthereumConfig } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { Asset } from '@vegaprotocol/react-helpers';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { useWeb3React } from '@web3-react/core';
export const useGetDepositedAmount = (asset: Asset | undefined) => {
const { account, provider } = useWeb3React();
const { config } = useEthereumConfig();
// For an explaination of how this code works see here: https://gist.github.com/emilbayes/44a36f59b06b1f3edb9cf914041544ed
const getDepositedAmount = useCallback(async () => {
if (
!provider ||
!config ||
!account ||
!asset ||
asset.source.__typename !== 'ERC20'
) {
return;
}
try {
const abicoder = new ethers.utils.AbiCoder();
const innerHash = ethers.utils.keccak256(
abicoder.encode(['address', 'uint256'], [account, 4])
);
const storageLocation = ethers.utils.keccak256(
abicoder.encode(
['address', 'bytes32'],
[asset.source.contractAddress, innerHash]
)
);
const res = await provider.getStorageAt(
config.collateral_bridge_contract.address,
storageLocation
);
const value = new BigNumber(res, 16).toString();
return new BigNumber(addDecimal(value, asset.decimals));
} catch (err) {
Sentry.captureException(err);
return;
}
}, [provider, asset, config, account]);
return getDepositedAmount;
};
+34 -3
View File
@@ -1,10 +1,41 @@
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/react-helpers';
import * as Sentry from '@sentry/react';
import type { Token } from '@vegaprotocol/smart-contracts';
import { useEthereumTransaction } from '@vegaprotocol/web3';
import {
useEthereumConfig,
useEthereumTransaction,
useTokenContract,
} from '@vegaprotocol/web3';
import { useDepositStore } from './deposit-store';
import { useGetAllowance } from './use-get-allowance';
export const useSubmitApproval = (contract: Token | null) => {
export const useSubmitApproval = () => {
const { config } = useEthereumConfig();
const { asset, update } = useDepositStore();
const contract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
true
);
const getAllowance = useGetAllowance(contract, asset);
const transaction = useEthereumTransaction<Token, 'approve'>(
contract,
'approve'
);
return transaction;
return {
...transaction,
perform: async () => {
if (!asset || !config) return;
try {
const amount = removeDecimal('1000000', asset.decimals);
await transaction.perform(
config.collateral_bridge_contract.address,
amount
);
const allowance = await getAllowance();
update({ allowance });
} catch (err) {
Sentry.captureException(err);
}
},
};
};
+34 -7
View File
@@ -1,21 +1,29 @@
import { gql, useSubscription } from '@apollo/client';
import * as Sentry from '@sentry/react';
import type {
DepositEvent,
DepositEventVariables,
} from './__generated__/DepositEvent';
import { DepositStatus } from '@vegaprotocol/types';
import { useState } from 'react';
import { remove0x } from '@vegaprotocol/react-helpers';
import {
isAssetTypeERC20,
remove0x,
removeDecimal,
} from '@vegaprotocol/react-helpers';
import {
useBridgeContract,
useEthereumConfig,
useEthereumTransaction,
useTokenContract,
} from '@vegaprotocol/web3';
import type {
CollateralBridge,
CollateralBridgeNew,
} from '@vegaprotocol/smart-contracts';
import { prepend0x } from '@vegaprotocol/smart-contracts';
import { useDepositStore } from './deposit-store';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
const DEPOSIT_EVENT_SUB = gql`
subscription DepositEvent($partyId: ID!) {
@@ -32,17 +40,24 @@ const DEPOSIT_EVENT_SUB = gql`
`;
export const useSubmitDeposit = () => {
const { asset, update } = useDepositStore();
const { config } = useEthereumConfig();
const contract = useBridgeContract(true);
const bridgeContract = useBridgeContract(true);
const tokenContract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
true
);
// Store public key from contract arguments for use in the subscription,
// NOTE: it may be different from the users connected key
const [partyId, setPartyId] = useState<string | null>(null);
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
const transaction = useEthereumTransaction<
CollateralBridgeNew | CollateralBridge,
'deposit_asset'
>(contract, 'deposit_asset', config?.confirmations, true);
>(bridgeContract, 'deposit_asset', config?.confirmations, true);
useSubscription<DepositEvent, DepositEventVariables>(DEPOSIT_EVENT_SUB, {
variables: { partyId: partyId ? remove0x(partyId) : '' },
@@ -78,10 +93,22 @@ export const useSubmitDeposit = () => {
return {
...transaction,
perform: (...args: Parameters<typeof transaction.perform>) => {
setPartyId(args[2]);
const publicKey = prepend0x(args[2]);
transaction.perform(args[0], args[1], publicKey);
perform: async (args: {
assetSource: string;
amount: string;
vegaPublicKey: string;
}) => {
if (!asset) return;
try {
setPartyId(args.vegaPublicKey);
const publicKey = prepend0x(args.vegaPublicKey);
const amount = removeDecimal(args.amount, asset.decimals);
await transaction.perform(args.assetSource, amount, publicKey);
const balance = await getBalance();
update({ balance });
} catch (err) {
Sentry.captureException(err);
}
},
};
};
+25 -4
View File
@@ -1,10 +1,31 @@
import type { Token, TokenFaucetable } from '@vegaprotocol/smart-contracts';
import { useEthereumTransaction } from '@vegaprotocol/web3';
import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
import * as Sentry from '@sentry/react';
import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3';
import { useDepositStore } from './deposit-store';
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
export const useSubmitFaucet = (contract: Token | TokenFaucetable | null) => {
export const useSubmitFaucet = () => {
const { asset, update } = useDepositStore();
const contract = useTokenContract(
isAssetTypeERC20(asset) ? asset : undefined,
true
);
const getBalance = useGetBalanceOfERC20Token(contract, asset);
const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>(
contract,
'faucet'
);
return transaction;
return {
...transaction,
perform: async () => {
try {
await transaction.perform();
const balance = await getBalance();
update({ balance });
} catch (err) {
Sentry.captureException(err);
}
},
};
};
+2
View File
@@ -12,6 +12,8 @@ The environment variables needed to be present for any app consuming this librar
`NX_VEGA_URL` OR `NX_VEGA_CONFIG_URL` - either the network configuration url or a url to a node to directly connect to
`NX_VEGA_WALLET_URL` the default vega wallet URL
For examples, see Block Explorer's .env files [here](../../apps/explorer)
## Running unit tests

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