Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fce401e8a1 | ||
|
|
8445996be5 | ||
|
|
e33ebb8fa9 | ||
|
|
ec69e1cc5d | ||
|
|
aeea1dff9d | ||
|
|
4e2b6b2d04 | ||
|
|
c29087cc96 | ||
|
|
5bde096977 | ||
|
|
923dc09eb6 | ||
|
|
4fbdb337dd | ||
|
|
6f4a5b9097 | ||
|
|
0c26d99ce2 | ||
|
|
5aedeba4ff | ||
|
|
5789d3496b | ||
|
|
d95fdca0d4 | ||
|
|
e4bf61c2e2 | ||
|
|
0ba54cd8a4 | ||
|
|
eee85c231b | ||
|
|
afdb387742 | ||
|
|
63698fbb80 | ||
|
|
58d8f7857e | ||
|
|
0931730582 |
@@ -18,9 +18,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Use Node.js 16
|
||||
id: Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 16.15.1
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4
|
||||
|
||||
@@ -48,7 +48,7 @@ const MarketLink = ({
|
||||
<Link
|
||||
className="underline"
|
||||
{...props}
|
||||
to={`/${Routes.MARKETS}#${id}`}
|
||||
to={`/${Routes.MARKETS}/${id}`}
|
||||
title={id}
|
||||
>
|
||||
{label}
|
||||
@@ -56,7 +56,7 @@ const MarketLink = ({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
|
||||
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ module.exports = defineConfig({
|
||||
viewportHeight: 900,
|
||||
responseTimeout: 50000,
|
||||
requestTimeout: 20000,
|
||||
retries: 2,
|
||||
},
|
||||
env: {
|
||||
ETHERSCAN_URL: 'https://sepolia.etherscan.io',
|
||||
|
||||
@@ -72,7 +72,11 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
|
||||
cy.getByTestId('deposit-approve-submit').click();
|
||||
cy.getByTestId('approve-warning').should(
|
||||
'contain.text',
|
||||
`Deposits of ${btcSymbol} not approved`
|
||||
);
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10');
|
||||
@@ -407,7 +411,7 @@ describe('capsule', { tags: '@slow' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
connectEthereumWallet('Unknown');
|
||||
cy.get(assetSelectField, txTimeout).select(vegaName, { force: true });
|
||||
cy.getByTestId('deposit-approve-submit').click();
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
|
||||
cy.get('[data-testid="Return to deposit"]').click();
|
||||
cy.get(amountField).clear().type('10000');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { removeDecimal } from '@vegaprotocol/cypress';
|
||||
import { ethers } from 'ethers';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
@@ -9,7 +11,7 @@ const formFieldError = 'input-error-text';
|
||||
const ASSET_EURO = 1;
|
||||
|
||||
describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
function openDepositForm() {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockSubscription();
|
||||
cy.mockTradingPage();
|
||||
@@ -20,10 +22,14 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('deposit-button').click();
|
||||
cy.wait('@Assets');
|
||||
connectEthereumWallet('MetaMask');
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
}
|
||||
|
||||
before(() => {
|
||||
openDepositForm();
|
||||
});
|
||||
|
||||
it('handles empty fields', () => {
|
||||
cy.getByTestId('deposit-submit').click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
cy.getByTestId(formFieldError).should('have.length', 2);
|
||||
});
|
||||
@@ -44,6 +50,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('invalid amount', () => {
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
// Deposit amount smaller than minimum viable for selected asset
|
||||
// Select an amount so that we have a known decimal places value to work with
|
||||
selectAsset(ASSET_EURO);
|
||||
@@ -56,12 +69,16 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('insufficient funds', () => {
|
||||
// 1001-DEPO-005
|
||||
// Deposit amount is valid, but less than approved. This will always be the case because our
|
||||
// CI wallet wont have approved any assets
|
||||
mockWeb3DepositCalls({
|
||||
allowance: '1000',
|
||||
depositLifetimeLimit: '1000',
|
||||
balance: '800',
|
||||
deposited: '0',
|
||||
dps: 5,
|
||||
});
|
||||
cy.get(amountField)
|
||||
.clear()
|
||||
.type('100')
|
||||
.type('850')
|
||||
.next(`[data-testid="${formFieldError}"]`)
|
||||
.should('have.text', 'Insufficient amount in Ethereum wallet');
|
||||
});
|
||||
@@ -88,3 +105,90 @@ describe('deposit actions', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('deposit-submit').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
function mockWeb3DepositCalls({
|
||||
allowance,
|
||||
depositLifetimeLimit,
|
||||
balance,
|
||||
deposited,
|
||||
dps,
|
||||
}: {
|
||||
allowance: string;
|
||||
depositLifetimeLimit: string;
|
||||
balance: string;
|
||||
deposited: string;
|
||||
dps: number;
|
||||
}) {
|
||||
const assetContractAddress = '0x0158031158bb4df2ad02eaa31e8963e84ea978a4';
|
||||
const collateralBridgeAddress = '0x7fe27d970bc8afc3b11cc8d9737bfb66b1efd799';
|
||||
const toResult = (value: string, dps: number) => {
|
||||
const rawValue = removeDecimal(value, dps);
|
||||
return ethers.utils.hexZeroPad(
|
||||
ethers.utils.hexlify(parseInt(rawValue)),
|
||||
32
|
||||
);
|
||||
};
|
||||
cy.intercept('POST', 'http://localhost:8545', (req) => {
|
||||
// Mock chainId call
|
||||
if (req.body.method === 'eth_chainId') {
|
||||
req.alias = 'eth_chainId';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: '0xaa36a7', // 11155111 for sepolia chain id
|
||||
});
|
||||
}
|
||||
|
||||
// Mock deposited amount
|
||||
if (req.body.method === 'eth_getStorageAt') {
|
||||
req.alias = 'eth_getStorageAt';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(deposited, dps),
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.method === 'eth_call') {
|
||||
// Mock approved amount for asset on collateral bridge
|
||||
if (
|
||||
req.body.params[0].to === assetContractAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0xdd62ed3e000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f0000000000000000000000007fe27d970bc8afc3b11cc8d9737bfb66b1efd799'
|
||||
) {
|
||||
req.alias = 'eth_call_allowance';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(allowance, dps),
|
||||
});
|
||||
}
|
||||
// Mock balance of asset in Ethereum wallet
|
||||
else if (
|
||||
req.body.params[0].to === assetContractAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0x70a08231000000000000000000000000ee7d375bcb50c26d52e1a4a472d8822a2a22d94f'
|
||||
) {
|
||||
req.alias = 'eth_call_balanceOf';
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(balance, dps),
|
||||
});
|
||||
}
|
||||
// Mock deposit lifetime limit
|
||||
else if (
|
||||
req.body.params[0].to === collateralBridgeAddress &&
|
||||
req.body.params[0].data ===
|
||||
'0x354a897a0000000000000000000000000158031158bb4df2ad02eaa31e8963e84ea978a4'
|
||||
) {
|
||||
req.alias = 'eth_call_get_deposit_maximum'; // deposit lifetime limit
|
||||
req.reply({
|
||||
id: req.body.id,
|
||||
jsonrpc: req.body.jsonrpc,
|
||||
result: toResult(depositLifetimeLimit, dps),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { marketsQuery } from '@vegaprotocol/mock';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const dialogCloseBtn = 'dialog-close';
|
||||
const popoverTrigger = 'popover-trigger';
|
||||
|
||||
describe('markets table', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -173,8 +174,13 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
function openMarketDropDown() {
|
||||
cy.getByTestId(dialogCloseBtn).should('be.visible');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
cy.getByTestId('popover-trigger').click();
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
cy.getByTestId(dialogCloseBtn).then((button) => {
|
||||
if (button.is(':visible')) {
|
||||
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
}
|
||||
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
|
||||
cy.getByTestId(popoverTrigger).click({ force: true });
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,19 +17,50 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('can connect', () => {
|
||||
// Mock authentication
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
token: 'test-token',
|
||||
},
|
||||
});
|
||||
// Mock getting keys from wallet
|
||||
cy.intercept('GET', 'https://wallet.testnet.vega.xyz/api/v1/keys', {
|
||||
body: {
|
||||
keys: [
|
||||
{
|
||||
algorithm: {
|
||||
name: 'algo',
|
||||
version: 1,
|
||||
},
|
||||
index: 0,
|
||||
meta: [],
|
||||
pub: 'HOSTED_PUBKEY',
|
||||
tainted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
mockConnectWallet();
|
||||
cy.contains('Connect Vega wallet');
|
||||
cy.contains('Hosted Fairground wallet');
|
||||
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-jsonRpc"]')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
.click();
|
||||
cy.wait('@walletReq');
|
||||
cy.getByTestId(form).find('#wallet').click().type('user');
|
||||
cy.getByTestId(form).find('#passphrase').click().type('pass');
|
||||
cy.getByTestId('rest-connector-form').find('button[type=submit]').click();
|
||||
cy.getByTestId(manageVegaBtn).should('exist');
|
||||
});
|
||||
|
||||
it('doesnt connect with invalid credentials', () => {
|
||||
// Mock incorrect username/password
|
||||
cy.intercept('POST', 'https://wallet.testnet.vega.xyz/api/v1/auth/token', {
|
||||
body: {
|
||||
error: 'No wallet',
|
||||
},
|
||||
statusCode: 403, // 403 forbidden invalid crednetials
|
||||
});
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
@@ -37,10 +68,10 @@ describe('connect hosted wallet', { tags: '@smoke' }, () => {
|
||||
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', 'No wallet detected');
|
||||
cy.getByTestId('form-error').should('have.text', 'Invalid credentials');
|
||||
});
|
||||
|
||||
it('doesnt connect with invalid fields', () => {
|
||||
it('doesnt connect with empty fields', () => {
|
||||
cy.getByTestId(connectVegaBtn).click();
|
||||
cy.getByTestId('connectors-list')
|
||||
.find('[data-testid="connector-hosted"]')
|
||||
|
||||
@@ -3,12 +3,14 @@ query AccountHistory(
|
||||
$assetId: ID!
|
||||
$accountTypes: [AccountType!]
|
||||
$dateRange: DateRange
|
||||
$marketIds: [ID!]
|
||||
) {
|
||||
balanceChanges(
|
||||
filter: {
|
||||
partyIds: [$partyId]
|
||||
accountTypes: $accountTypes
|
||||
assetId: $assetId
|
||||
marketIds: $marketIds
|
||||
}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AccountHistoryQueryVariables = Types.Exact<{
|
||||
assetId: Types.Scalars['ID'];
|
||||
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -23,9 +24,9 @@ export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: {
|
||||
|
||||
|
||||
export const AccountHistoryDocument = gql`
|
||||
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange) {
|
||||
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
|
||||
balanceChanges(
|
||||
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId}
|
||||
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
edges {
|
||||
@@ -58,6 +59,7 @@ export const AccountHistoryDocument = gql`
|
||||
* assetId: // value for 'assetId'
|
||||
* accountTypes: // value for 'accountTypes'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* marketIds: // value for 'marketIds'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import compact from 'lodash/compact';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -25,8 +26,10 @@ import {
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { PriceChart } from 'pennant';
|
||||
import 'pennant/dist/style.css';
|
||||
import { accountsOnlyDataProvider } from '@vegaprotocol/accounts';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { accountsDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
|
||||
const DateRange = {
|
||||
RANGE_1D: '1D',
|
||||
@@ -97,7 +100,7 @@ const AccountHistoryManager = ({
|
||||
);
|
||||
|
||||
const { data: accounts } = useDataProvider({
|
||||
dataProvider: accountsOnlyDataProvider,
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: variablesForOneTimeQuery,
|
||||
skip: !pubKey,
|
||||
});
|
||||
@@ -118,6 +121,39 @@ const AccountHistoryManager = ({
|
||||
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
|
||||
DateRange.RANGE_1M
|
||||
);
|
||||
const [market, setMarket] = useState<Market | null>(null);
|
||||
const marketFilterCb = useCallback(
|
||||
(item: Market) =>
|
||||
!asset?.id ||
|
||||
item.tradableInstrument.instrument.product.settlementAsset.id ===
|
||||
asset?.id,
|
||||
[asset?.id]
|
||||
);
|
||||
const markets = useMemo<Market[] | null>(() => {
|
||||
const arr =
|
||||
accounts
|
||||
?.filter((item: Account) => Boolean(item && item.market))
|
||||
.map<Market>((item) => item.market as Market) ?? null;
|
||||
return arr
|
||||
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
|
||||
a.tradableInstrument.instrument.code.localeCompare(
|
||||
b.tradableInstrument.instrument.code
|
||||
)
|
||||
)
|
||||
: null;
|
||||
}, [accounts, marketFilterCb]);
|
||||
const resolveMarket = useCallback(
|
||||
(m: Market) => {
|
||||
setMarket(m);
|
||||
const newAssetId =
|
||||
m.tradableInstrument.instrument.product.settlementAsset.id;
|
||||
const newAsset = assets.find((item) => item.id === newAssetId);
|
||||
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
|
||||
setAsset(newAsset);
|
||||
}
|
||||
},
|
||||
[asset, assets]
|
||||
);
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
@@ -126,62 +162,113 @@ const AccountHistoryManager = ({
|
||||
accountTypes: accountType ? [accountType] : undefined,
|
||||
dateRange:
|
||||
range === 'All' ? undefined : { start: calculateStartDate(range) },
|
||||
marketIds: market?.id ? [market.id] : undefined,
|
||||
}),
|
||||
[pubKey, asset, accountType, range]
|
||||
[pubKey, asset, accountType, range, market?.id]
|
||||
);
|
||||
|
||||
const { data } = useAccountHistoryQuery({
|
||||
variables,
|
||||
skip: !asset || !pubKey,
|
||||
});
|
||||
|
||||
const accountTypeMenu = useMemo(() => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_BOND,
|
||||
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
].map((type) => (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
onClick={() => setAccountType(type as Schema.AccountType)}
|
||||
>
|
||||
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}, [accountType]);
|
||||
const assetsMenu = useMemo(() => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{assets.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
|
||||
{a.symbol}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}, [assets, asset]);
|
||||
const marketsMenu = useMemo(() => {
|
||||
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
|
||||
markets?.length ? (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{market
|
||||
? market.tradableInstrument.instrument.code
|
||||
: t('Select market')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{market && (
|
||||
<DropdownMenuItem key="0" onClick={() => setMarket(null)}>
|
||||
{t('All markets')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{markets?.map((m) => (
|
||||
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
|
||||
{m.tradableInstrument.instrument.code}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null;
|
||||
}, [markets, market, accountType, resolveMarket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
|
||||
market?.tradableInstrument.instrument.product.settlementAsset.id !==
|
||||
asset?.id
|
||||
) {
|
||||
setMarket(null);
|
||||
}
|
||||
}, [accountType, asset?.id, market]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col gap-8">
|
||||
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_BOND,
|
||||
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
].map((type) => (
|
||||
<DropdownMenuItem
|
||||
key={type}
|
||||
onClick={() => setAccountType(type as Schema.AccountType)}
|
||||
>
|
||||
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{assets.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
|
||||
{a.symbol}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<>
|
||||
{accountTypeMenu}
|
||||
{assetsMenu}
|
||||
{marketsMenu}
|
||||
</>
|
||||
</div>
|
||||
<div className="pt-1 justify-items-end">
|
||||
<Toggle
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ReactNode } from 'react';
|
||||
import { AppFailure } from './app-failure';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
|
||||
const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
loading: () => <>Loading...</>,
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import { Connectors } from '../lib/vega-connectors';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { Banner } from '../components/banner';
|
||||
import classNames from 'classnames';
|
||||
import { AppLoader } from '../components/app-loader';
|
||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@@ -115,7 +115,7 @@ function VegaTradingApp(props: AppProps) {
|
||||
// Prevent HashRouter from being server side rendered as it
|
||||
// relies on presence of document object
|
||||
if (status === 'default') {
|
||||
return null;
|
||||
return <DynamicLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,8 @@ export default function Document() {
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/preloader.css" media="all" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(0) {
|
||||
animation-delay: 0ms;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:first-child {
|
||||
animation-delay: -0.2s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(4) {
|
||||
animation-delay: 1s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(5) {
|
||||
animation-delay: -0.25s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(6) {
|
||||
animation-delay: 0.3s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(7) {
|
||||
animation-delay: -1.05s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(8) {
|
||||
animation-delay: 0.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(9) {
|
||||
animation-delay: -0.9s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(10) {
|
||||
animation-delay: 0.5s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(11) {
|
||||
animation-delay: -2.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(12) {
|
||||
animation-delay: 2.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(13) {
|
||||
animation-delay: -0.65s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(14) {
|
||||
animation-delay: 0.7s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(15) {
|
||||
animation-delay: -3s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(16) {
|
||||
animation-delay: 2.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader .pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #000;
|
||||
animation: flickering 0.4s steps(2, jump-none) infinite alternate;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -318,33 +318,37 @@ const SummaryMessage = memo(
|
||||
}
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'md',
|
||||
}}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
testId={'deal-ticket-connect-wallet'}
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<p className="text-sm pb-2">
|
||||
You need a{' '}
|
||||
<ExternalLink href="https://vega.xyz/wallet">
|
||||
Vega wallet
|
||||
</ExternalLink>{' '}
|
||||
with {assetSymbol} to start trading in this market.
|
||||
</p>
|
||||
}
|
||||
buttonProps={{
|
||||
text: t('Connect wallet'),
|
||||
action: openVegaWalletDialog,
|
||||
dataTestId: 'order-connect-wallet',
|
||||
size: 'md',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||
return (
|
||||
<ZeroBalanceError
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<ZeroBalanceError
|
||||
asset={market.tradableInstrument.instrument.product.settlementAsset}
|
||||
onClickCollateral={onClickCollateral}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -363,7 +367,11 @@ const SummaryMessage = memo(
|
||||
// If there is no blocking error but user doesn't have enough
|
||||
// balance render the margin warning, but still allow submission
|
||||
if (balanceError) {
|
||||
return <MarginWarning balance={balance} margin={margin} asset={asset} />;
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<MarginWarning balance={balance} margin={margin} asset={asset} />;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show auction mode warning
|
||||
@@ -375,13 +383,15 @@ const SummaryMessage = memo(
|
||||
].includes(marketData.marketTradingMode)
|
||||
) {
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId={'dealticket-warning-auction'}
|
||||
message={t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
)}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId={'dealticket-warning-auction'}
|
||||
message={t(
|
||||
'Any orders placed now will not trade until the auction ends'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import type { DepositFormProps } from './deposit-form';
|
||||
import { DepositForm } from './deposit-form';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
jest.mock('@vegaprotocol/wallet');
|
||||
jest.mock('@vegaprotocol/web3');
|
||||
jest.mock('@web3-react/core');
|
||||
|
||||
const mockConnector = { deactivate: jest.fn() };
|
||||
@@ -37,6 +39,8 @@ function generateAsset(): AssetFieldsFragment {
|
||||
let asset: AssetFieldsFragment;
|
||||
let props: DepositFormProps;
|
||||
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
|
||||
const MOCK_VEGA_KEY =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
|
||||
beforeEach(() => {
|
||||
asset = generateAsset();
|
||||
@@ -89,14 +93,17 @@ describe('Deposit form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when submitted with invalid ethereum address', async () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({ account: '123' });
|
||||
it('fails when Ethereum wallet not connected', async () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: false,
|
||||
account: '',
|
||||
});
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Invalid Ethereum address')
|
||||
await screen.findByText('Connect Ethereum wallet')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -138,7 +145,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Insufficient amount in Ethereum wallet')
|
||||
await screen.findByText('Amount is above deposit limit')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -159,7 +166,7 @@ describe('Deposit form', () => {
|
||||
fireEvent.submit(screen.getByTestId('deposit-form'));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Amount is above approved amount')
|
||||
await screen.findByText('Amount is above approved amount.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -193,9 +200,9 @@ describe('Deposit form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles deposit approvals', () => {
|
||||
it('handles deposit approvals', async () => {
|
||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||
mockUseVegaWallet.mockReturnValue({ pubKey: null });
|
||||
mockUseVegaWallet.mockReturnValue({ pubKey: MOCK_VEGA_KEY });
|
||||
|
||||
const mockUseWeb3React = useWeb3React as jest.Mock;
|
||||
mockUseWeb3React.mockReturnValue({
|
||||
@@ -212,13 +219,18 @@ describe('Deposit form', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByText(`Approve ${asset.symbol}`, {
|
||||
selector: '[type="button"]',
|
||||
})
|
||||
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
|
||||
`Deposits of ${asset.symbol} not approved`
|
||||
);
|
||||
|
||||
expect(props.submitApprove).toHaveBeenCalled();
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: `Approve ${asset.symbol}` })
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.submitApprove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles submitting a deposit', async () => {
|
||||
@@ -284,4 +296,55 @@ describe('Deposit form', () => {
|
||||
render(<DepositForm {...props} />);
|
||||
expect(await screen.queryAllByTestId('view-asset-details')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders a connect button if Ethereum wallet is not connected', () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: false,
|
||||
account: '',
|
||||
});
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Connect' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText('From (Ethereum address)')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a disabled input if Ethereum wallet is connected', () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: true,
|
||||
account: MOCK_ETH_ADDRESS,
|
||||
});
|
||||
render(<DepositForm {...props} />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Connect' })
|
||||
).not.toBeInTheDocument();
|
||||
const fromInput = screen.getByLabelText('From (Ethereum address)');
|
||||
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
|
||||
expect(fromInput).toBeDisabled();
|
||||
expect(fromInput).toHaveAttribute('readonly');
|
||||
});
|
||||
|
||||
it('prevents submission if you are on the wrong chain', () => {
|
||||
(useWeb3React as jest.Mock).mockReturnValue({
|
||||
isActive: true,
|
||||
account: MOCK_ETH_ADDRESS,
|
||||
chainId: 1,
|
||||
});
|
||||
(useWeb3ConnectStore as unknown as jest.Mock).mockImplementation(
|
||||
// eslint-disable-next-line
|
||||
(selector: (result: ReturnType<typeof useWeb3ConnectStore>) => any) => {
|
||||
return selector({
|
||||
desiredChainId: 11155111,
|
||||
open: jest.fn(),
|
||||
foo: 'asdf',
|
||||
});
|
||||
}
|
||||
);
|
||||
render(<DepositForm {...props} />);
|
||||
expect(screen.getByTestId('chain-error')).toHaveTextContent(
|
||||
/this app only works on/i
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { AssetOption } from '@vegaprotocol/assets';
|
||||
import {
|
||||
ethereumAddress,
|
||||
t,
|
||||
ethereumAddress,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
minSafe,
|
||||
@@ -14,17 +14,19 @@ import {
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Icon,
|
||||
Input,
|
||||
InputError,
|
||||
RichSelect,
|
||||
Notification,
|
||||
Intent,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import type { FieldError } from 'react-hook-form';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { DepositLimits } from './deposit-limits';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import {
|
||||
@@ -72,7 +74,8 @@ export const DepositForm = ({
|
||||
isFaucetable,
|
||||
}: DepositFormProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const { account } = useWeb3React();
|
||||
const openDialog = useWeb3ConnectStore((store) => store.open);
|
||||
const { isActive, account } = useWeb3React();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const {
|
||||
register,
|
||||
@@ -83,26 +86,27 @@ export const DepositForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
from: account,
|
||||
to: pubKey ? pubKey : undefined,
|
||||
asset: selectedAsset?.id || '',
|
||||
},
|
||||
});
|
||||
|
||||
const onDeposit = async (fields: FormFields) => {
|
||||
const onSubmit = async (fields: FormFields) => {
|
||||
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
|
||||
throw new Error('Invalid asset');
|
||||
}
|
||||
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
if (approved) {
|
||||
submitDeposit({
|
||||
assetSource: selectedAsset.source.contractAddress,
|
||||
amount: fields.amount,
|
||||
vegaPublicKey: fields.to,
|
||||
});
|
||||
} else {
|
||||
submitApprove();
|
||||
}
|
||||
};
|
||||
|
||||
const amount = useWatch({ name: 'amount', control });
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
const maxApproved = allowance ? allowance : new BigNumber(0);
|
||||
const maxAvailable = balance ? balance : new BigNumber(0);
|
||||
@@ -133,9 +137,12 @@ export const DepositForm = ({
|
||||
return minViableAmount;
|
||||
}, [selectedAsset]);
|
||||
|
||||
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
|
||||
const formState = getFormState(selectedAsset, isActive, approved);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onDeposit)}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
noValidate={true}
|
||||
data-testid="deposit-form"
|
||||
>
|
||||
@@ -143,19 +150,54 @@ export const DepositForm = ({
|
||||
label={t('From (Ethereum address)')}
|
||||
labelFor="ethereum-address"
|
||||
>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
{...register('from', {
|
||||
<Controller
|
||||
name="from"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
required: (value) => {
|
||||
if (!value) return t('Connect Ethereum wallet');
|
||||
return true;
|
||||
},
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<EthereumButton
|
||||
clearAddress={() => {
|
||||
setValue('from', '');
|
||||
clearErrors('from');
|
||||
}}
|
||||
defaultValue={account}
|
||||
render={() => {
|
||||
if (isActive && account) {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="ethereum-address"
|
||||
value={account}
|
||||
readOnly={true}
|
||||
disabled={true}
|
||||
{...register('from', {
|
||||
validate: {
|
||||
required,
|
||||
ethereumAddress,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<DisconnectEthereumButton
|
||||
onDisconnect={() => {
|
||||
setValue('from', ''); // clear from value so required ethereum connection validation works
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onClick={openDialog}
|
||||
variant="primary"
|
||||
fill={true}
|
||||
type="button"
|
||||
data-testid="connect-eth-wallet-btn"
|
||||
>
|
||||
{t('Connect')}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.from?.message && (
|
||||
@@ -241,148 +283,136 @@ export const DepositForm = ({
|
||||
deposited={deposited}
|
||||
balance={balance}
|
||||
asset={selectedAsset}
|
||||
allowance={allowance}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
} else if (value.isGreaterThan(maxAmount.limit)) {
|
||||
return t('Amount is above temporary deposit limit');
|
||||
} else if (value.isGreaterThan(maxAmount.approved)) {
|
||||
return t('Amount is above approved amount');
|
||||
}
|
||||
return maxSafe(maxAmount.amount)(v);
|
||||
{formState === 'deposit' && (
|
||||
<FormGroup label={t('Amount')} labelFor="amount">
|
||||
<Input
|
||||
type="number"
|
||||
autoComplete="off"
|
||||
id="amount"
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
approved: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.approved)) {
|
||||
return t('Amount is above approved amount');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
limit: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.limit)) {
|
||||
return t('Amount is above deposit limit');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
balance: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(maxAmount.available)) {
|
||||
return t('Insufficient amount in Ethereum wallet');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
return maxSafe(maxAmount.amount)(v);
|
||||
},
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{errors.amount.message}
|
||||
</InputError>
|
||||
)}
|
||||
{selectedAsset && balance && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormButton
|
||||
selectedAsset={selectedAsset}
|
||||
amount={new BigNumber(amount || 0)}
|
||||
allowance={allowance}
|
||||
onApproveClick={submitApprove}
|
||||
/>
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<AmountError error={errors.amount} submitApprove={submitApprove} />
|
||||
)}
|
||||
{selectedAsset && balance && (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
setValue('amount', balance.toFixed(selectedAsset.decimals));
|
||||
clearErrors('amount');
|
||||
}}
|
||||
>
|
||||
{t('Use maximum')}
|
||||
</UseButton>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormButton selectedAsset={selectedAsset} formState={formState} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const AmountError = ({
|
||||
error,
|
||||
submitApprove,
|
||||
}: {
|
||||
error: FieldError;
|
||||
submitApprove: () => void;
|
||||
}) => {
|
||||
if (error.type === 'approved') {
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}.
|
||||
<button onClick={submitApprove} className="underline ml-2">
|
||||
{t('Update approve amount')}
|
||||
</button>
|
||||
</InputError>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<InputError intent="danger" forInput="amount">
|
||||
{error.message}
|
||||
</InputError>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormButtonProps {
|
||||
selectedAsset?: Asset;
|
||||
amount: BigNumber;
|
||||
allowance: BigNumber | undefined;
|
||||
onApproveClick: () => void;
|
||||
formState: ReturnType<typeof getFormState>;
|
||||
}
|
||||
|
||||
const FormButton = ({
|
||||
selectedAsset,
|
||||
amount,
|
||||
allowance,
|
||||
onApproveClick,
|
||||
}: FormButtonProps) => {
|
||||
const { open, desiredChainId } = useWeb3ConnectStore((store) => ({
|
||||
open: store.open,
|
||||
desiredChainId: store.desiredChainId,
|
||||
}));
|
||||
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
|
||||
const { isActive, chainId } = useWeb3React();
|
||||
const approved =
|
||||
allowance && allowance.isGreaterThan(0) && amount.isLessThan(allowance);
|
||||
let button = null;
|
||||
let message: ReactNode = '';
|
||||
|
||||
if (!isActive) {
|
||||
button = (
|
||||
<Button onClick={open} data-testid="connect-eth-wallet-btn">
|
||||
{t('Connect Ethereum wallet')}
|
||||
</Button>
|
||||
);
|
||||
} else if (chainId !== desiredChainId) {
|
||||
const chainName = getChainName(desiredChainId);
|
||||
message = t(`This app only works on ${chainName}.`);
|
||||
button = (
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
disabled={true}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else if (!selectedAsset) {
|
||||
button = (
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else if (approved) {
|
||||
message = (
|
||||
<>
|
||||
<Icon name="tick" className="mr-2" />
|
||||
<span>{t('Approved')}</span>
|
||||
</>
|
||||
);
|
||||
button = (
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
message = t(`Deposits of ${selectedAsset.symbol} not approved`);
|
||||
button = (
|
||||
<Button
|
||||
onClick={onApproveClick}
|
||||
data-testid="deposit-approve-submit"
|
||||
variant="primary"
|
||||
fill={true}
|
||||
>
|
||||
{t(`Approve ${selectedAsset.symbol}`)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
||||
const submitText =
|
||||
formState === 'approve'
|
||||
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
|
||||
: t('Deposit');
|
||||
const invalidChain = isActive && chainId !== desiredChainId;
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{message && <p className="text-center">{message}</p>}
|
||||
{button}
|
||||
</div>
|
||||
<>
|
||||
{formState === 'approve' && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
testId="approve-warning"
|
||||
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{invalidChain && (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Danger}
|
||||
testId="chain-error"
|
||||
message={t(
|
||||
`This app only works on ${getChainName(desiredChainId)}.`
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="deposit-submit"
|
||||
variant={isActive ? 'primary' : 'default'}
|
||||
fill={true}
|
||||
disabled={invalidChain}
|
||||
>
|
||||
{submitText}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -398,21 +428,20 @@ const UseButton = (props: UseButtonProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
|
||||
const openDialog = useWeb3ConnectStore((state) => state.open);
|
||||
const { isActive, connector } = useWeb3React();
|
||||
const DisconnectEthereumButton = ({
|
||||
onDisconnect,
|
||||
}: {
|
||||
onDisconnect: () => void;
|
||||
}) => {
|
||||
const { connector } = useWeb3React();
|
||||
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
||||
|
||||
if (!isActive) {
|
||||
return <UseButton onClick={openDialog}>{t('Connect')}</UseButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<UseButton
|
||||
onClick={() => {
|
||||
connector.deactivate();
|
||||
clearAddress();
|
||||
removeEagerConnector();
|
||||
onDisconnect();
|
||||
}}
|
||||
data-testid="disconnect-ethereum-wallet"
|
||||
>
|
||||
@@ -420,3 +449,14 @@ const EthereumButton = ({ clearAddress }: { clearAddress: () => void }) => {
|
||||
</UseButton>
|
||||
);
|
||||
};
|
||||
|
||||
const getFormState = (
|
||||
selectedAsset: Asset | undefined,
|
||||
isActive: boolean,
|
||||
approved: boolean
|
||||
) => {
|
||||
if (!selectedAsset) return 'deposit';
|
||||
if (!isActive) return 'deposit';
|
||||
if (approved) return 'deposit';
|
||||
return 'approve';
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ interface DepositLimitsProps {
|
||||
deposited: BigNumber;
|
||||
asset: Asset;
|
||||
balance?: BigNumber;
|
||||
allowance?: BigNumber;
|
||||
}
|
||||
|
||||
export const DepositLimits = ({
|
||||
@@ -18,6 +19,7 @@ export const DepositLimits = ({
|
||||
deposited,
|
||||
asset,
|
||||
balance,
|
||||
allowance,
|
||||
}: DepositLimitsProps) => {
|
||||
const limits = [
|
||||
{
|
||||
@@ -44,6 +46,12 @@ export const DepositLimits = ({
|
||||
rawValue: max.minus(deposited),
|
||||
value: compactNumber(max.minus(deposited), asset.decimals),
|
||||
},
|
||||
{
|
||||
key: 'ALLOWANCE',
|
||||
label: t('Approved'),
|
||||
rawValue: allowance,
|
||||
value: allowance ? compactNumber(allowance, asset.decimals) : '-',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -45,8 +45,9 @@ export const FillsManager = ({
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
noRowsOverlayComponent={() => null}
|
||||
onMarketClick={onMarketClick}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -27,12 +27,13 @@ export const AssetProposalNotification = ({
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="asset-proposal-notification"
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="asset-proposal-notification"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ export const MarketProposalNotification = ({
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
testId="market-proposal-notification"
|
||||
className="px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,8 @@ import type { Asset } from '@vegaprotocol/assets';
|
||||
import { assetsProvider } from '@vegaprotocol/assets';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||
import type { PageInfo } from '@vegaprotocol/react-helpers';
|
||||
import { makeInfiniteScrollGetRows } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
defaultAppend as append,
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
useDataProvider,
|
||||
@@ -27,13 +25,15 @@ import type {
|
||||
import { LedgerEntriesDocument } from './__generated__/LedgerEntries';
|
||||
|
||||
export type LedgerEntry = LedgerEntryFragment & {
|
||||
id: number;
|
||||
asset: Asset | null | undefined;
|
||||
marketSender: Market | null | undefined;
|
||||
marketReceiver: Market | null | undefined;
|
||||
};
|
||||
|
||||
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge;
|
||||
export type AggregatedLedgerEntriesNode = AggregatedLedgerEntriesEdge & {
|
||||
node: LedgerEntry;
|
||||
};
|
||||
|
||||
const getData = (responseData: LedgerEntriesQuery | null) => {
|
||||
return responseData?.ledgerEntries?.edges || [];
|
||||
@@ -94,33 +94,29 @@ export const update = (
|
||||
});
|
||||
};
|
||||
|
||||
const getPageInfo = (responseData: LedgerEntriesQuery): PageInfo | null =>
|
||||
responseData.ledgerEntries?.pageInfo || null;
|
||||
|
||||
const ledgerEntriesOnlyProvider = makeDataProvider({
|
||||
query: LedgerEntriesDocument,
|
||||
getData,
|
||||
getDelta: getData,
|
||||
update,
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 100,
|
||||
},
|
||||
additionalContext: {
|
||||
isEnlargedTimeout: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const ledgerEntriesProvider = makeDerivedDataProvider<
|
||||
(AggregatedLedgerEntriesEdge | null)[],
|
||||
AggregatedLedgerEntriesEdge[],
|
||||
AggregatedLedgerEntriesNode[],
|
||||
AggregatedLedgerEntriesNode[],
|
||||
LedgerEntriesQueryVariables
|
||||
>(
|
||||
[ledgerEntriesOnlyProvider, assetsProvider, marketsProvider],
|
||||
[
|
||||
ledgerEntriesOnlyProvider,
|
||||
(callback, client) => assetsProvider(callback, client),
|
||||
marketsProvider,
|
||||
],
|
||||
([entries, assets, markets]) => {
|
||||
return entries.map((edge: AggregatedLedgerEntriesEdge) => {
|
||||
const entry = edge?.node;
|
||||
const entry = edge.node;
|
||||
const asset = assets.find((asset: Asset) => asset.id === entry.assetId);
|
||||
const marketSender = markets.find(
|
||||
(market: Market) => market.id === entry.fromAccountMarketId
|
||||
@@ -148,21 +144,22 @@ export const useLedgerEntriesDataProvider = ({
|
||||
filter,
|
||||
gridRef,
|
||||
}: Props) => {
|
||||
const dataRef = useRef<(AggregatedLedgerEntriesEdge | null)[] | null>(null);
|
||||
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
|
||||
const totalCountRef = useRef<number>();
|
||||
|
||||
const variables = useMemo<LedgerEntriesQueryVariables>(
|
||||
() => ({
|
||||
partyId,
|
||||
dateRange: filter?.vegaTime?.value,
|
||||
fromAccountType: filter?.fromAccountType?.value ?? null,
|
||||
toAccountType: filter?.toAccountType?.value ?? null,
|
||||
pagination: {
|
||||
first: 5000,
|
||||
},
|
||||
}),
|
||||
[partyId, filter]
|
||||
[partyId, filter?.vegaTime?.value]
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: (AggregatedLedgerEntriesEdge | null)[] | null }) => {
|
||||
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
|
||||
return updateGridData(dataRef, data, gridRef);
|
||||
},
|
||||
[gridRef]
|
||||
@@ -173,7 +170,7 @@ export const useLedgerEntriesDataProvider = ({
|
||||
data,
|
||||
totalCount,
|
||||
}: {
|
||||
data: (AggregatedLedgerEntriesEdge | null)[] | null;
|
||||
data: AggregatedLedgerEntriesEdge[] | null;
|
||||
totalCount?: number;
|
||||
}) => {
|
||||
totalCountRef.current = totalCount;
|
||||
|
||||
@@ -3,7 +3,9 @@ import type * as Schema from '@vegaprotocol/types';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { subDays, formatRFC3339 } from 'date-fns';
|
||||
import type { AggregatedLedgerEntriesNode } from './ledger-entries-data-provider';
|
||||
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider';
|
||||
import { LedgerTable } from './ledger-table';
|
||||
import type * as Types from '@vegaprotocol/types';
|
||||
@@ -15,43 +17,43 @@ export interface Filter {
|
||||
fromAccountType?: { value: Types.AccountType[] };
|
||||
toAccountType?: { value: Types.AccountType[] };
|
||||
}
|
||||
|
||||
type LedgerManagerProps = { partyId: string };
|
||||
export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
const defaultFilter = {
|
||||
vegaTime: {
|
||||
value: { start: formatRFC3339(subDays(Date.now(), 7)) },
|
||||
},
|
||||
};
|
||||
export const LedgerManager = ({ partyId }: { partyId: string }) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [filter, setFilter] = useState<Filter | undefined>();
|
||||
const [filter, setFilter] = useState<Filter>(defaultFilter);
|
||||
const [dataCount, setDataCount] = useState(0);
|
||||
|
||||
const { data, error, loading, getRows, reload } =
|
||||
useLedgerEntriesDataProvider({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
});
|
||||
const { data, error, loading, reload } = useLedgerEntriesDataProvider({
|
||||
partyId,
|
||||
filter,
|
||||
gridRef,
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
const updatedFilter = event.api.getFilterModel();
|
||||
if (Object.keys(updatedFilter).length) {
|
||||
setFilter(updatedFilter);
|
||||
} else if (filter) {
|
||||
setFilter(undefined);
|
||||
}
|
||||
},
|
||||
[filter]
|
||||
);
|
||||
const getRowId = useCallback(
|
||||
({ data }: { data: Types.AggregatedLedgerEntry }) =>
|
||||
`${data.vegaTime}-${data.fromAccountPartyId}-${data.toAccountPartyId}`,
|
||||
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
|
||||
const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() };
|
||||
setFilter(updatedFilter);
|
||||
}, []);
|
||||
const extractNodesDecorator = useCallback(
|
||||
(data: AggregatedLedgerEntriesNode[] | null, loading: boolean) =>
|
||||
data && !loading ? data.map((item) => item.node) : null,
|
||||
[]
|
||||
);
|
||||
|
||||
const extractedData = extractNodesDecorator(data, loading);
|
||||
useEffect(() => {
|
||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||
}, [extractedData]);
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<LedgerTable
|
||||
ref={gridRef}
|
||||
rowModelType="infinite"
|
||||
datasource={{ getRows }}
|
||||
rowData={extractedData}
|
||||
onFilterChanged={onFilterChanged}
|
||||
getRowId={getRowId}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
@@ -59,7 +61,7 @@ export const LedgerManager = ({ partyId }: LedgerManagerProps) => {
|
||||
error={error}
|
||||
data={data}
|
||||
noDataMessage={t('No entries')}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
noDataCondition={() => !dataCount}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '@vegaprotocol/types';
|
||||
import type { LedgerEntry } from './ledger-entries-data-provider';
|
||||
import { forwardRef } from 'react';
|
||||
import { formatRFC3339, subDays } from 'date-fns';
|
||||
|
||||
export const TransferTooltipCellComponent = ({
|
||||
value,
|
||||
@@ -35,6 +36,11 @@ export const TransferTooltipCellComponent = ({
|
||||
);
|
||||
};
|
||||
|
||||
const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) };
|
||||
const dateRangeFilterParams = {
|
||||
maxNextDays: 0,
|
||||
defaultRangeFilter,
|
||||
};
|
||||
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
|
||||
|
||||
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
@@ -42,17 +48,20 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
return (
|
||||
<AgGrid
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
overlayNoRowsTemplate={t('No entries')}
|
||||
ref={ref}
|
||||
getRowId={({ data }) => data.id}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
tooltipComponent: TransferTooltipCellComponent,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
filterParams: {
|
||||
...dateRangeFilterParams,
|
||||
buttons: ['reset'],
|
||||
},
|
||||
}}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
{...props}
|
||||
>
|
||||
<AgGridColumn
|
||||
@@ -201,6 +210,7 @@ export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>(
|
||||
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
|
||||
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-'
|
||||
}
|
||||
filterParams={dateRangeFilterParams}
|
||||
filter={DateRangeFilter}
|
||||
/>
|
||||
</AgGrid>
|
||||
|
||||
@@ -74,7 +74,7 @@ export const FeesBreakdown = ({
|
||||
: '-';
|
||||
};
|
||||
return (
|
||||
<dl className="grid grid-cols-5">
|
||||
<dl className="grid grid-cols-6">
|
||||
<dt className="col-span-2">{t('Infrastructure fee')}</dt>
|
||||
{feeFactors && (
|
||||
<dd className="text-right col-span-1">
|
||||
@@ -83,7 +83,7 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.infrastructureFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Liquidity fee')}</dt>
|
||||
@@ -94,7 +94,7 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.liquidityFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Maker fee')}</dt>
|
||||
@@ -105,7 +105,7 @@ export const FeesBreakdown = ({
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.makerFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Total fees')}</dt>
|
||||
@@ -114,7 +114,7 @@ export const FeesBreakdown = ({
|
||||
{totalFeesPercentage(feeFactors)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-2">
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(totalFees)} {symbol || ''}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
@@ -152,6 +152,8 @@ export const OrderListManager = ({
|
||||
isReadOnly={isReadOnly}
|
||||
hasActiveOrder={hasActiveOrder}
|
||||
blockLoadDebounceMillis={100}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable */
|
||||
process.env.TZ = 'UTC';
|
||||
export default {
|
||||
displayName: 'react-helpers',
|
||||
preset: '../../jest.preset.js',
|
||||
|
||||
@@ -10,7 +10,9 @@ const MIN_FRACTION_DIGITS = 2;
|
||||
const MAX_FRACTION_DIGITS = 20;
|
||||
|
||||
export function toDecimal(numberOfDecimals: number) {
|
||||
return 1 / Math.pow(10, numberOfDecimals);
|
||||
return new BigNumber(1)
|
||||
.dividedBy(Math.pow(10, numberOfDecimals))
|
||||
.toString(10);
|
||||
}
|
||||
|
||||
export function toBigNum(
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { DateRangeFilterProps } from './date-range-filter';
|
||||
import { DateRangeFilter } from './date-range-filter';
|
||||
|
||||
const commonProps = {
|
||||
filterChangedCallback: jest.fn(),
|
||||
};
|
||||
|
||||
describe('DateRangeFilter', () => {
|
||||
it('should be properly rendered', async () => {
|
||||
const defaultRangeFilter = {
|
||||
start: '2023-02-14T13:53:01+01:00',
|
||||
end: '2023-02-21T13:53:01+01:00',
|
||||
};
|
||||
const displayStartValue = '2023-02-14T12:53:01.000';
|
||||
const displayEndValue = '2023-02-21T12:53:01.000';
|
||||
render(
|
||||
<DateRangeFilter
|
||||
{...(commonProps as unknown as DateRangeFilterProps)}
|
||||
defaultRangeFilter={defaultRangeFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText('Start')).toHaveValue(displayStartValue);
|
||||
expect(screen.getByLabelText('End')).toHaveValue(displayEndValue);
|
||||
|
||||
expect(commonProps.filterChangedCallback).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,113 +1,259 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||
import { formatForInput } from '../format/date';
|
||||
import {
|
||||
isBefore,
|
||||
subDays,
|
||||
addDays,
|
||||
differenceInDays,
|
||||
formatRFC3339,
|
||||
min,
|
||||
max,
|
||||
isValid,
|
||||
} from 'date-fns';
|
||||
import { t } from '../i18n';
|
||||
import { formatForInput } from '../format/date';
|
||||
|
||||
const defaultFilterValue: Schema.DateRange = {};
|
||||
export interface DateRangeFilterProps extends IFilterParams {
|
||||
defaultRangeFilter?: Schema.DateRange;
|
||||
maxSubDays?: number;
|
||||
maxNextDays?: number;
|
||||
maxDaysRange?: number;
|
||||
}
|
||||
|
||||
export const DateRangeFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
const [value, setValue] = useState<Schema.DateRange>(defaultFilterValue);
|
||||
export const DateRangeFilter = forwardRef(
|
||||
(props: DateRangeFilterProps, ref) => {
|
||||
const defaultDates = props?.defaultRangeFilter || defaultFilterValue;
|
||||
const [value, setValue] = useState<Schema.DateRange>(defaultDates);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => {
|
||||
const minStartDate =
|
||||
props?.maxSubDays !== undefined
|
||||
? formatForInput(subDays(Date.now(), props.maxSubDays))
|
||||
: '';
|
||||
const maxStartDate =
|
||||
props?.maxNextDays !== undefined
|
||||
? formatForInput(addDays(Date.now(), props.maxNextDays))
|
||||
: '';
|
||||
const minEndDate =
|
||||
value.start && props?.maxDaysRange !== undefined
|
||||
? formatForInput(new Date(value.start))
|
||||
: minStartDate || value.start
|
||||
? formatForInput(new Date(value.start))
|
||||
: '';
|
||||
const endDateCandidates = [];
|
||||
if (props.maxNextDays !== undefined) {
|
||||
endDateCandidates.push(addDays(new Date(), props.maxNextDays));
|
||||
}
|
||||
if (props.maxDaysRange !== undefined && value.start) {
|
||||
endDateCandidates.push(
|
||||
addDays(new Date(value.start), props.maxDaysRange)
|
||||
);
|
||||
}
|
||||
const maxEndDate = endDateCandidates.length
|
||||
? formatForInput(min(endDateCandidates))
|
||||
: maxStartDate;
|
||||
return [minStartDate, maxStartDate, minEndDate, maxEndDate];
|
||||
}, [props.maxSubDays, props.maxDaysRange, props.maxNextDays, value.start]);
|
||||
// expose AG Grid Filter Lifecycle callbacks
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
const rowValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
if (
|
||||
value.start &&
|
||||
rowValue &&
|
||||
new Date(rowValue) <= new Date(value.start)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
value.end &&
|
||||
rowValue &&
|
||||
new Date(rowValue) >= new Date(value.end)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// expose AG Grid Filter Lifecycle callbacks
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
const rowValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
if (
|
||||
value.start &&
|
||||
rowValue &&
|
||||
new Date(rowValue) <= new Date(value.start)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
value.end &&
|
||||
rowValue &&
|
||||
new Date(rowValue) >= new Date(value.end)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isFilterActive() {
|
||||
return value.start || value.end;
|
||||
},
|
||||
|
||||
isFilterActive() {
|
||||
return value.start || value.end;
|
||||
},
|
||||
getModel() {
|
||||
if (!this.isFilterActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
if (!this.isFilterActive()) {
|
||||
return null;
|
||||
}
|
||||
return { value };
|
||||
},
|
||||
|
||||
return { value };
|
||||
},
|
||||
|
||||
setModel(model?: { value: Schema.DateRange } | null) {
|
||||
setValue(model?.value || defaultFilterValue);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setValue({
|
||||
...value,
|
||||
[event.target.name]:
|
||||
event.target.value &&
|
||||
new Date(event.target.value).toISOString().replace('Z', '000000Z'),
|
||||
setModel(model?: { value: Schema.DateRange } | null) {
|
||||
setValue(
|
||||
model?.value || props?.defaultRangeFilter || defaultFilterValue
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
const validate = (
|
||||
name: string,
|
||||
timeValue: Date,
|
||||
update?: Schema.DateRange
|
||||
) => {
|
||||
if (
|
||||
props.maxSubDays !== undefined &&
|
||||
isBefore(new Date(timeValue), subDays(Date.now(), props.maxSubDays + 1))
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The earliest data that can be queried is %s days ago.',
|
||||
String(props.maxSubDays)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (props?.maxDaysRange !== undefined) {
|
||||
const contrvalue =
|
||||
name === 'start'
|
||||
? update?.end || value.end
|
||||
: update?.start || value.start;
|
||||
if (
|
||||
Math.abs(
|
||||
differenceInDays(new Date(timeValue), new Date(contrvalue))
|
||||
) > props.maxDaysRange
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The maximum time range that can be queried is %s days.',
|
||||
String(props.maxDaysRange)
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setError('');
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props?.filterChangedCallback();
|
||||
}, [value, props]);
|
||||
const checkForEndDate = (
|
||||
endDate: Date | undefined,
|
||||
startDate: Date | undefined
|
||||
) => {
|
||||
const endDateCandidates: Date[] = [];
|
||||
if (props.maxDaysRange !== undefined && isValid(startDate)) {
|
||||
endDateCandidates.push(addDays(startDate as Date, props.maxDaysRange));
|
||||
}
|
||||
if (props.maxNextDays !== undefined) {
|
||||
endDateCandidates.push(addDays(Date.now(), props.maxNextDays));
|
||||
}
|
||||
if (isValid(endDate)) {
|
||||
endDateCandidates.push(endDate as Date);
|
||||
}
|
||||
return endDate && startDate
|
||||
? formatRFC3339(max([startDate, min(endDateCandidates)]))
|
||||
: undefined;
|
||||
};
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value: dateValue, name } = event.target;
|
||||
const date = new Date(dateValue || defaultDates[name as 'start' | 'end']);
|
||||
let update = { [name]: isValid(date) ? formatRFC3339(date) : undefined };
|
||||
const startCheckDate = name === 'start' ? date : new Date(value.start);
|
||||
const endCheckDate =
|
||||
name === 'start'
|
||||
? new Date(value.end)
|
||||
: isValid(date)
|
||||
? date
|
||||
: new Date(maxEndDate);
|
||||
const endDate = isValid(endCheckDate) ? endCheckDate : undefined;
|
||||
const startDate = isValid(startCheckDate) ? startCheckDate : undefined;
|
||||
update = { ...update, end: checkForEndDate(endDate, startDate) };
|
||||
|
||||
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
||||
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
||||
return (
|
||||
<div className="ag-filter-body-wrapper">
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="start">
|
||||
<span className="block">{t('Start')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start"
|
||||
value={start}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="block" key="end">
|
||||
<span className="block">{t('End')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end"
|
||||
value={end}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<div className="ag-filter-apply-panel">
|
||||
<button
|
||||
type="button"
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => setValue(defaultFilterValue)}
|
||||
if (validate(name, date, update)) {
|
||||
setValue((curr) => ({
|
||||
...curr,
|
||||
...update,
|
||||
}));
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
props?.filterChangedCallback();
|
||||
}, [value, props]);
|
||||
|
||||
const notification = useMemo(() => {
|
||||
const not = error ? (
|
||||
<div
|
||||
className="text-sm flex items-center first-letter:uppercase mt-2 border-danger text-danger"
|
||||
role="alert"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{error}
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
<div className="ag-filter-apply-panel flex min-h-[2rem]">{not}</div>
|
||||
);
|
||||
}, [error]);
|
||||
|
||||
const start = (value.start && formatForInput(new Date(value.start))) || '';
|
||||
const end = (value.end && formatForInput(new Date(value.end))) || '';
|
||||
return (
|
||||
<div className="ag-filter-body-wrapper inline-block min-w-fit">
|
||||
{notification}
|
||||
<div className="ag-filter-apply-panel">
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="start">
|
||||
<span className="block mb-1">{t('Start')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="start"
|
||||
value={start || ''}
|
||||
onChange={onChange}
|
||||
min={minStartDate}
|
||||
max={maxStartDate}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset className="ag-simple-filter-body-wrapper">
|
||||
<label className="block" key="end">
|
||||
<span className="block mb-1">{t('End')}</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="end"
|
||||
value={end || ''}
|
||||
onChange={onChange}
|
||||
min={minEndDate}
|
||||
max={maxEndDate}
|
||||
/>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="ag-filter-apply-panel">
|
||||
<button
|
||||
type="button"
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => {
|
||||
setError('');
|
||||
setValue(defaultDates);
|
||||
}}
|
||||
>
|
||||
{t('Reset')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||
import { t } from '../i18n';
|
||||
|
||||
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
const [value, setValue] = useState<string[]>([]);
|
||||
@@ -16,18 +17,19 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
doesFilterPass(params: IDoesFilterPassParams) {
|
||||
const { api, colDef, column, columnApi, context } = props;
|
||||
const { node } = params;
|
||||
return (
|
||||
props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
}) === value
|
||||
);
|
||||
const getValue = props.valueGetter({
|
||||
api,
|
||||
colDef,
|
||||
column,
|
||||
columnApi,
|
||||
context,
|
||||
data: node.data,
|
||||
getValue: (field) => node.data[field],
|
||||
node,
|
||||
});
|
||||
return Array.isArray(value)
|
||||
? value.includes(getValue)
|
||||
: getValue === value;
|
||||
},
|
||||
|
||||
isFilterActive() {
|
||||
@@ -83,7 +85,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
|
||||
className="ag-standard-button ag-filter-apply-panel-button"
|
||||
onClick={() => setValue([])}
|
||||
>
|
||||
Reset
|
||||
{t('Reset')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from '../i18n';
|
||||
|
||||
export const validateAmount = (step: number, field: string) => {
|
||||
export const validateAmount = (step: number | string, field: string) => {
|
||||
const [, stepDecimals = ''] = String(step).split('.');
|
||||
|
||||
return (value: string) => {
|
||||
|
||||
Generated
+4
@@ -2761,12 +2761,16 @@ export type PositionUpdate = {
|
||||
__typename?: 'PositionUpdate';
|
||||
/** Average entry price for this position */
|
||||
averageEntryPrice: Scalars['String'];
|
||||
/** The total amount of profit and loss that was not transferred due to loss socialisation */
|
||||
lossSocializationAmount: Scalars['String'];
|
||||
/** Market relating to this position */
|
||||
marketId: Scalars['ID'];
|
||||
/** Open volume (int64) */
|
||||
openVolume: Scalars['String'];
|
||||
/** The party holding this position */
|
||||
partyId: Scalars['ID'];
|
||||
/** Enum set if the position was closed out or orders were removed because party was distressed */
|
||||
positionStatus: PositionStatus;
|
||||
/** Realised Profit and Loss (int64) */
|
||||
realisedPNL: Scalars['String'];
|
||||
/** Unrealised Profit and Loss (int64) */
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
const pseudoRandom = (seed: number) => {
|
||||
let value = seed;
|
||||
return () => {
|
||||
value = (value * 16807) % 2147483647;
|
||||
return value / 1000000000;
|
||||
};
|
||||
};
|
||||
|
||||
export interface LoaderProps {
|
||||
size?: 'small' | 'large';
|
||||
@@ -35,6 +43,8 @@ export const Loader = ({
|
||||
size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]';
|
||||
const items = size === 'small' ? 9 : 16;
|
||||
|
||||
const generate = useMemo(() => pseudoRandom(1), []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center pre-loader-center"
|
||||
@@ -47,7 +57,7 @@ export const Loader = ({
|
||||
className={itemClasses}
|
||||
key={i}
|
||||
style={{
|
||||
opacity: Math.random() > 0.75 ? 1 : 0,
|
||||
opacity: generate() > 1.5 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ type NotificationProps = {
|
||||
size?: ButtonSize;
|
||||
};
|
||||
testId?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const getIcon = (intent: Intent): IconName => {
|
||||
@@ -39,7 +38,6 @@ export const Notification = ({
|
||||
title,
|
||||
testId,
|
||||
buttonProps,
|
||||
className,
|
||||
}: NotificationProps) => {
|
||||
return (
|
||||
<div
|
||||
@@ -61,8 +59,7 @@ export const Notification = ({
|
||||
intent === Intent.Warning,
|
||||
'bg-vega-pink-300 dark:bg-vega-pink-650': intent === Intent.Danger,
|
||||
},
|
||||
'border rounded p-2 flex items-start gap-2.5 my-4',
|
||||
className
|
||||
'border rounded p-2 flex items-start gap-2.5'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -28,7 +28,7 @@ export const Tabs = ({ children, active: activeDefaultId }: TabsProps) => {
|
||||
if (!isValidElement(child) || child.props.hidden) return null;
|
||||
const isActive = child.props.id === activeTab;
|
||||
const triggerClass = classNames(
|
||||
'relative px-4 py-2 border-r border-default',
|
||||
'relative px-4 py-1 border-r border-default',
|
||||
'uppercase',
|
||||
{
|
||||
'cursor-default': isActive,
|
||||
|
||||
Reference in New Issue
Block a user