Compare commits

...
Author SHA1 Message Date
Matthew Russell 8d0cc27401 chore: update pr template to not close issues 2023-08-29 07:19:59 -07:00
MaciekandMatthew Russell 9ac199f59a chore(ledger): remove ledger entries table, adjust download form (#4611)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-29 06:57:02 -07:00
Edd 332cc302c3 feat(explorer): add stop order view (#4418) 2023-08-29 12:24:22 +02:00
Edd 4da0e9c368 fix(explorer): correctly display data source matches (#4631) 2023-08-29 12:23:55 +02:00
Joe Tsang 13e1f99767 test(governance): successor market proposal changes (#4632) 2023-08-29 09:47:59 +01:00
ArtandMatthew Russell 5c18c898b0 feat(wallet): metamask snaps (#4621)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-28 18:51:58 -07:00
Matthew Russell 4fe81cc4aa fix(trading): update fonts for pennant (#4636) 2023-08-28 10:02:32 -07:00
Art 0fb8ee3abb fix(proposals): parsing block duration value (#4613) 2023-08-28 13:11:24 +00:00
Matthew Russell 3422b99491 feat(trading): change to system monospace font (#4634) 2023-08-25 15:24:57 -07:00
Radosław Szpiech 287e294281 chore(trading): remove market-all-cy.ts as it was moved to console-test (#4633) 2023-08-25 14:28:42 -07:00
Bartłomiej GłowniaandMatthew Russell dc959025c6 fix(deal-ticket): disable iceberg for IOC and FOK (#4629)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-25 14:27:55 -07:00
Ben 63bfcc8f65 chore(trading): remove deal ticket basic submit (#4628) 2023-08-25 10:57:09 -07:00
Bartłomiej Głownia 952e906eac fix(deal-ticket): show expiresAt field value from state only, add required validation (#4626) 2023-08-25 10:56:43 -07:00
Matthew Russell e765c247ef chore(trading): adjust the get started checkboxes slightly (#4622) 2023-08-25 10:52:52 -07:00
Sam KeenandJoe 52dea6d0dc feat(ui-toolkit,governance): description preview and read more pattern (#4599)
Co-authored-by: Joe <joe@vega.xyz>
2023-08-25 16:46:03 +01:00
Ben 97f243e5f7 chore(trading): show test run time (#4623) 2023-08-25 11:04:38 +01:00
daro-maj 9992d9f053 test(trading): remove markets proposed tests (#4618) 2023-08-25 12:00:04 +02:00
Sam Keen 3e26431e8f feat(governance): small UX improvements to successor market panels (#4562) 2023-08-25 09:58:01 +00:00
Bartłomiej Głownia 6a9f15f59e feat(deal-ticket): submit oco stop orders (#4539) 2023-08-25 08:37:14 +02:00
Edd 4684745382 fix(explorer): properly render issue signatures tx (#4610) 2023-08-24 18:51:55 +01:00
Joe Tsang 927e21b045 test(explorer): add e2e test for asset proposal on explorer (#4616) 2023-08-24 18:51:24 +01:00
129 changed files with 3857 additions and 3256 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Related issues 🔗
Closes #[Issue number here]
Issue: #[Issue number here]
# Description
+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest -s --numprocesses auto --dist loadfile
run: poetry run pytest -v -s --numprocesses auto --dist loadfile --durations=20
- name: Check files
run: |
ls -al .
@@ -0,0 +1,26 @@
{
"proposalSubmission": {
"rationale": {
"title": "Test new asset proposal",
"description": "E2E test for proposals"
},
"terms": {
"newAsset": {
"changes": {
"name": "USDT Coin",
"symbol": "USDT",
"decimals": "18",
"quantum": "1",
"erc20": {
"contractAddress": "0xb404c51bbc10dcbe948077f18a4b8e553d160084",
"withdrawThreshold": "10",
"lifetimeLimit": "10"
}
}
},
"closingTimestamp": 1724339572,
"enactmentTimestamp": 1724339572,
"validationTimestamp": 1692799617
}
}
}
@@ -1,9 +1,10 @@
import { getNewAssetTxBody } from '../support/governance.functions';
context('Proposal page', { tags: '@smoke' }, function () {
describe('Verify elements on page', function () {
const proposalHeading = 'proposals-heading';
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
const proposalTitle = 'Add Lorem Ipsum market';
before('Create market proposal', function () {
cy.visit('/');
@@ -11,6 +12,8 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
it('Able to view proposal', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.navigate_to('governanceProposals');
cy.getByTestId(proposalHeading).should('be.visible');
cy.contains(proposalTitle)
@@ -22,6 +25,9 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
@@ -35,9 +41,12 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
cy.getByTestId('dialog-title').should('have.text', proposalTitle);
cy.get('.language-json').should('exist');
cy.getByTestId('icon-cross').click();
});
it.skip('Proposal page displayed on mobile', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.common_switch_to_mobile_and_click_toggle();
cy.navigate_to('governanceProposals', true);
cy.getByTestId(proposalHeading).should('be.visible');
@@ -45,5 +54,40 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
});
});
it('Able to view new asset proposal', function () {
const proposalTitle = 'Test new asset proposal';
const newAssetProposalBody = getNewAssetTxBody();
cy.VegaWalletSubmitProposal(newAssetProposalBody);
cy.visit('/');
cy.navigate_to('governanceProposals');
cy.contains(proposalTitle)
.parent()
.parent()
.parent()
.within(() => {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewAsset');
cy.get_element_by_col_id('state').should(
'have.text',
'Waiting for Node Vote'
);
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.get('[col-id="eDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and('contains', 'https://governance.fairground.wtf/proposals/');
cy.contains('View terms').should('exist').click();
});
});
});
});
@@ -1,8 +1,18 @@
import { addSeconds, millisecondsToSeconds } from 'date-fns';
export function createSuccessorMarketProposal(parentMarketId) {
cy.VegaWalletSubmitProposal(getSuccessorTxBody(parentMarketId));
}
function getSuccessorTxBody(parentMarketId) {
const MIN_CLOSE_SEC = 500;
const MIN_ENACT_SEC = 700;
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
return {
proposalSubmission: {
rationale: {
@@ -122,8 +132,49 @@ function getSuccessorTxBody(parentMarketId) {
},
},
},
closingTimestamp: 1695666618,
enactmentTimestamp: 1695666618,
closingTimestamp,
enactmentTimestamp,
},
},
};
}
export function getNewAssetTxBody() {
const MIN_CLOSE_SEC = 500;
const MIN_ENACT_SEC = 700;
const MIN_VALID_SEC = 60;
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
const validationDate = addSeconds(new Date(), MIN_VALID_SEC);
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
const validationTimestamp = millisecondsToSeconds(validationDate.getTime());
return {
proposalSubmission: {
rationale: {
title: 'Test new asset proposal',
description: 'E2E test for proposals',
},
terms: {
newAsset: {
changes: {
name: 'USDT Coin',
symbol: 'USDT',
decimals: '18',
quantum: '1',
erc20: {
contractAddress: '0xb404c51bbc10dcbe948077f18a4b8e553d160084',
withdrawThreshold: '10',
lifetimeLimit: '10',
},
},
},
closingTimestamp,
enactmentTimestamp,
validationTimestamp,
},
},
};
+1 -1
View File
@@ -77,7 +77,7 @@
"executor": "nx:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/spec-update-v0.72.0-preview.2/specs/v0.72.0-preview.2/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.72.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
]
}
},
@@ -0,0 +1,25 @@
fragment ExplorerStopOrderFields on StopOrder {
id
status
createdAt
trigger {
... on StopOrderPrice {
price
}
... on StopOrderTrailingPercentOffset {
trailingPercentOffset
}
}
createdAt
ocoLinkId
triggerDirection
order {
id
}
}
query ExplorerStopOrder($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...ExplorerStopOrderFields
}
}
@@ -0,0 +1,70 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerStopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, status: Types.StopOrderStatus, createdAt: any, ocoLinkId?: string | null, triggerDirection: Types.StopOrderTriggerDirection, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, order?: { __typename?: 'Order', id: string } | null };
export type ExplorerStopOrderQueryVariables = Types.Exact<{
stopOrderId: Types.Scalars['ID'];
}>;
export type ExplorerStopOrderQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, status: Types.StopOrderStatus, createdAt: any, ocoLinkId?: string | null, triggerDirection: Types.StopOrderTriggerDirection, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, order?: { __typename?: 'Order', id: string } | null } | null };
export const ExplorerStopOrderFieldsFragmentDoc = gql`
fragment ExplorerStopOrderFields on StopOrder {
id
status
createdAt
trigger {
... on StopOrderPrice {
price
}
... on StopOrderTrailingPercentOffset {
trailingPercentOffset
}
}
createdAt
ocoLinkId
triggerDirection
order {
id
}
}
`;
export const ExplorerStopOrderDocument = gql`
query ExplorerStopOrder($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...ExplorerStopOrderFields
}
}
${ExplorerStopOrderFieldsFragmentDoc}`;
/**
* __useExplorerStopOrderQuery__
*
* To run a query within a React component, call `useExplorerStopOrderQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerStopOrderQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerStopOrderQuery({
* variables: {
* stopOrderId: // value for 'stopOrderId'
* },
* });
*/
export function useExplorerStopOrderQuery(baseOptions: Apollo.QueryHookOptions<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>(ExplorerStopOrderDocument, options);
}
export function useExplorerStopOrderLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>(ExplorerStopOrderDocument, options);
}
export type ExplorerStopOrderQueryHookResult = ReturnType<typeof useExplorerStopOrderQuery>;
export type ExplorerStopOrderLazyQueryHookResult = ReturnType<typeof useExplorerStopOrderLazyQuery>;
export type ExplorerStopOrderQueryResult = Apollo.QueryResult<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>;
@@ -33,7 +33,7 @@ describe('Order TX Summary component', () => {
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
});
it('Renders nothing if the order passed lacks a price', () => {
it('Renders "Market Price" if the order passed lacks a price', () => {
const o: Order = {
marketId: '123',
side: 'SIDE_BUY',
@@ -41,7 +41,7 @@ describe('Order TX Summary component', () => {
size: '10',
};
const res = renderComponent(o);
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
expect(res.queryByText('Market Price')).toBeInTheDocument();
});
it('Renders nothing if the order has an unspecified side', () => {
@@ -3,6 +3,7 @@ import type { components } from '../../../types/explorer';
import PriceInMarket from '../price-in-market/price-in-market';
import { sideText } from '../order-details/lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market';
import { t } from '@vegaprotocol/i18n';
export type OrderSummaryProps = {
order: components['schemas']['v1OrderSubmission'];
@@ -20,7 +21,6 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
if (
!order ||
!order.marketId ||
!order.price ||
!order.side ||
order.side === 'SIDE_UNSPECIFIED'
) {
@@ -36,10 +36,14 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
'-'
)}
&nbsp;<i className="text-xs">@</i>&nbsp;
<PriceInMarket
marketId={order.marketId}
price={order.price}
></PriceInMarket>
{order.price ? (
<PriceInMarket
marketId={order.marketId}
price={order.price}
></PriceInMarket>
) : (
t('Market Price')
)}
</div>
);
};
@@ -0,0 +1,139 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../types/explorer';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import PriceInMarket from '../../../price-in-market/price-in-market';
import StopOrderTriggerSummary from './stop-order-trigger';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import fromUnixTime from 'date-fns/fromUnixTime';
const wrapperClasses =
'flex-1 max-w-xs items-center border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
export type StopOrderType = 'RisesAbove' | 'FallsBelow' | 'OCO';
type V1OrderSetup = components['schemas']['v1StopOrderSetup'];
interface StopOrderSetupProps extends V1OrderSetup {
type: StopOrderType;
deterministicId: string;
}
export function getExpiryTypeLabel(
expiryStrategy: V1OrderSetup['expiryStrategy']
): string {
switch (expiryStrategy) {
case 'EXPIRY_STRATEGY_CANCELS':
return t('Cancels');
case 'EXPIRY_STRATEGY_SUBMIT':
return t('Submit');
}
return expiryStrategy || t('Unknown');
}
export interface ExpiryTriggerProps {
trailingPercentOffset?: string;
price?: string;
marketId?: string;
}
export function ExpiryTrigger({
trailingPercentOffset,
price,
marketId,
}: ExpiryTriggerProps) {
if (price && marketId) {
return <PriceInMarket price={price} marketId={marketId} />;
}
if (trailingPercentOffset) {
return (
<span>
{formatNumberPercentage(new BigNumber(trailingPercentOffset))}{' '}
(trailing)
</span>
);
}
return null;
}
export function getMovePrefix(
type: StopOrderType,
trailingPercentOffset?: string
): string {
if (type === 'RisesAbove') {
if (trailingPercentOffset) {
return '+';
} else {
return '>';
}
} else {
if (trailingPercentOffset) {
return '-';
} else {
return '<';
}
}
}
export const TypeLabel = {
RisesAbove: t('Rises above ↗'),
FallsBelow: t('Falls below ↘'),
OCO: '',
};
/**
*/
export const StopOrderSetup = ({
type,
price,
orderSubmission,
expiresAt,
expiryStrategy,
trailingPercentOffset,
deterministicId,
}: StopOrderSetupProps) => {
let d = 'Unknown';
try {
d = expiresAt
? fromUnixTime(parseInt(expiresAt) / 1000000000).toLocaleString()
: t('Unknown');
} catch (e) {
d = t('Unknown');
}
return (
<div className={wrapperClasses}>
<div className="mb-12 lg:mb-0">
<div className="bg-slate-100 text-slate-900 px-6 py-2 md:px-6 flex">
<div className="flex-1">
<strong className="font-bold mb-1">{TypeLabel[type]} </strong>
<p className=" font-xs mb-0">
{getMovePrefix(type, trailingPercentOffset)}
<ExpiryTrigger
trailingPercentOffset={trailingPercentOffset}
price={price}
marketId={orderSubmission?.marketId}
/>
</p>
</div>
{expiresAt && expiryStrategy ? (
<div className="flex-1">
<strong className="font-bold mb-1">{t('Expiry Type')}</strong>
<p className=" font-xs mb-0">
<Tooltip description={<span>{d}</span>}>
<span>{getExpiryTypeLabel(expiryStrategy)}</span>
</Tooltip>
</p>
</div>
) : null}
</div>
<StopOrderTriggerSummary
id={deterministicId}
orderSubmission={orderSubmission}
/>
</div>
</div>
);
};
@@ -0,0 +1,108 @@
import { StopOrderStatus } from '@vegaprotocol/types';
import { useExplorerStopOrderQuery } from '../../../order-details/__generated__/StopOrder';
import type { ExplorerStopOrderQuery } from '../../../order-details/__generated__/StopOrder';
import { t } from '@vegaprotocol/i18n';
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import OrderTxSummary from '../../../order-summary/order-tx-summary';
import type { components } from '../../../../../types/explorer';
import type { ApolloError } from '@apollo/client';
export const StatusLabel: Record<StopOrderStatus, string> = {
[StopOrderStatus.STATUS_CANCELLED]: t('Cancelled'),
[StopOrderStatus.STATUS_EXPIRED]: t('Expired'),
[StopOrderStatus.STATUS_PENDING]: t('Pending'),
[StopOrderStatus.STATUS_REJECTED]: t('Rejected'),
[StopOrderStatus.STATUS_STOPPED]: t('Stopped'),
[StopOrderStatus.STATUS_TRIGGERED]: t('Triggered'),
[StopOrderStatus.STATUS_UNSPECIFIED]: t('Status unknown'),
};
export const StatusIcon: Record<StopOrderStatus, IconName> = {
[StopOrderStatus.STATUS_CANCELLED]: 'disable',
[StopOrderStatus.STATUS_EXPIRED]: 'outdated',
[StopOrderStatus.STATUS_PENDING]: 'circle',
[StopOrderStatus.STATUS_REJECTED]: 'cross',
[StopOrderStatus.STATUS_STOPPED]: 'stop',
[StopOrderStatus.STATUS_TRIGGERED]: 'tick',
[StopOrderStatus.STATUS_UNSPECIFIED]: 'help',
};
export const StatusMidColor: Record<StopOrderStatus, string> = {
[StopOrderStatus.STATUS_CANCELLED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_EXPIRED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_PENDING]: 'bg-yellow-100 text-yellow-900',
[StopOrderStatus.STATUS_REJECTED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_STOPPED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_TRIGGERED]: 'bg-green-100 text-green-900',
[StopOrderStatus.STATUS_UNSPECIFIED]: 'bg-yellow-100 text-yellow-900',
};
export const StatusBottomColor: Record<StopOrderStatus, string> = {
[StopOrderStatus.STATUS_CANCELLED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_EXPIRED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_PENDING]: 'bg-yellow-50 text-yellow-900',
[StopOrderStatus.STATUS_REJECTED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_STOPPED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_TRIGGERED]: 'bg-green-50 text-green-900',
[StopOrderStatus.STATUS_UNSPECIFIED]:
'bg-yellow-50 text-yellow-900 line-through',
};
export function getStopOrderTriggerStatus(
data?: ExplorerStopOrderQuery,
error?: ApolloError
) {
if (data && data.stopOrder) {
return data.stopOrder.status;
}
return StopOrderStatus.STATUS_UNSPECIFIED;
}
export interface StopOrderTriggerSummaryProps {
id: string;
orderSubmission?: components['schemas']['v1OrderSubmission'];
}
/**
*/
const StopOrderTriggerSummary = ({
id,
orderSubmission,
}: StopOrderTriggerSummaryProps) => {
const { data, error } = useExplorerStopOrderQuery({
variables: { stopOrderId: id },
});
const status = getStopOrderTriggerStatus(data, error);
return (
<>
<div
className={`${StatusMidColor[status]} px-3 py-2 md:px-6 flex space-x-4`}
>
<p className="m-0 p-0 align-top">
<Icon
size={6}
name={StatusIcon[status]}
className="inline-block mr-2"
/>
<span className="align-top">{StatusLabel[status]}</span>
</p>
</div>
<div
className={`${StatusBottomColor[status]} px-3 py-2 md:px-6 flex space-x-4`}
>
{orderSubmission && (
<p className="text-vega-grey-400 strike">
<OrderTxSummary order={orderSubmission} />
</p>
)}
</div>
</>
);
};
export default StopOrderTriggerSummary;
@@ -26,6 +26,11 @@ export const sharedHeaderProps = {
className: 'align-top',
};
const Labels: Record<BlockExplorerTransactionResult['type'], string> = {
'Stop Orders Submission': 'Stop Order',
'Stop Orders Cancellation': 'Cancel Stop Order',
};
/**
* These rows are shown for every transaction type, providing a consistent set of rows for the top
* of a transaction details row. The order is relatively arbitrary but felt right - it might need to
@@ -44,12 +49,14 @@ export const TxDetailsShared = ({
const time: string = blockData?.result.block.header.time || '';
const height: string = blockData?.result.block.header.height || txData.block;
const type = Labels[txData.type] || txData.type;
return (
<>
{hideTypeRow === false ? (
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
<TableCell>{type}</TableCell>
</TableRow>
) : null}
<TableRow modifier="bordered">
@@ -31,6 +31,8 @@ const AccountType: Record<AccountTypes, string> = {
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: 'LP Received Fees',
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: 'Market Proposers',
ACCOUNT_TYPE_HOLDING: 'Holding',
ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: 'Bonus Distribution',
ACCOUNT_TYPE_LP_LIQUIDITY_FEES: 'LP Liquidity Fees',
};
interface TransferParticipantsProps {
@@ -27,6 +27,7 @@ import { TxDetailsNodeAnnounce } from './tx-node-announce';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
import { TxProposal } from './tx-proposal';
import { TxDetailsTransfer } from './tx-transfer';
import { TxDetailsStopOrderSubmission } from './tx-stop-order-submission';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -116,6 +117,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUndelegate;
case 'State Variable Proposal':
return TxDetailsStateVariable;
case 'Stop Orders Submission':
return TxDetailsStopOrderSubmission;
case 'Transfer Funds':
return TxDetailsTransfer;
default:
@@ -41,7 +41,7 @@ export const TxDetailsIssueSignatures = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const cmd: Command = txData.command;
const cmd: Command = txData.command.issueSignatures;
const k = cmd.kind ? kind[cmd.kind] : null;
return (
@@ -0,0 +1,148 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import { MarketLink } from '../../links/';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import {
getStopOrderIds,
stopOrdersSignatureToDeterministicId,
} from '../lib/deterministic-ids';
import type { components } from '../../../../types/explorer';
import { StopOrderSetup } from './order/stop-order-setup';
type StopOrderSetup = components['schemas']['v1StopOrderSetup'];
interface TxDetailsOrderProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
export function getStopTypeLabel(
risesAbove: StopOrderSetup | undefined,
fallsBelow: StopOrderSetup | undefined
): string {
if (risesAbove && fallsBelow) {
return t('OCO (One Cancels Other)');
} else if (fallsBelow) {
return t('Falls Below ↘');
} else if (risesAbove) {
return t('Rises Above ↗');
} else {
return t('Stop Order');
}
}
export interface StopMarketIdProps {
risesAbove: StopOrderSetup | undefined;
fallsBelow: StopOrderSetup | undefined;
showMarketName?: boolean;
}
export function StopMarketId({
risesAbove,
fallsBelow,
showMarketName = false,
}: StopMarketIdProps) {
const raMarketId = risesAbove?.orderSubmission?.marketId;
const fbMarketId = fallsBelow?.orderSubmission?.marketId;
if (raMarketId && fbMarketId) {
if (raMarketId === fbMarketId) {
return <MarketLink id={raMarketId} showMarketName={showMarketName} />;
} else {
return (
<>
<MarketLink id={raMarketId} showMarketName={showMarketName} />,
<MarketLink id={fbMarketId} showMarketName={showMarketName} />
</>
);
}
} else if (raMarketId) {
return <MarketLink id={raMarketId} showMarketName={showMarketName} />;
} else if (fbMarketId) {
return <MarketLink id={fbMarketId} showMarketName={showMarketName} />;
} else {
return '-';
}
}
/**
*/
export const TxDetailsStopOrderSubmission = ({
txData,
pubKey,
blockData,
}: TxDetailsOrderProps) => {
if (!txData || !txData.command.stopOrdersSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const tx: components['schemas']['v1StopOrdersSubmission'] =
txData.command.stopOrdersSubmission;
const orderIds = stopOrdersSignatureToDeterministicId(
txData?.signature?.value
);
const { risesAboveId, fallsBelowId } = getStopOrderIds(
orderIds,
tx.risesAbove,
tx.fallsBelow
);
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Market ID')}</TableCell>
<TableCell>
<StopMarketId
risesAbove={tx.risesAbove}
fallsBelow={tx.fallsBelow}
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<StopMarketId
risesAbove={tx.risesAbove}
fallsBelow={tx.fallsBelow}
showMarketName={true}
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Trigger')}</TableCell>
<TableCell>
{getStopTypeLabel(tx.risesAbove, tx.fallsBelow)}
</TableCell>
</TableRow>
</TableWithTbody>
<div className="flex gap-2">
{tx.fallsBelow && fallsBelowId && (
<StopOrderSetup
type={'FallsBelow'}
{...tx.fallsBelow}
deterministicId={fallsBelowId}
/>
)}
{tx.risesAbove && risesAboveId && (
<StopOrderSetup
type={'RisesAbove'}
{...tx.risesAbove}
deterministicId={risesAboveId}
/>
)}
</div>
</>
);
};
@@ -1,4 +1,8 @@
import { hexToString, txSignatureToDeterministicId } from './deterministic-ids';
import {
hexToString,
txSignatureToDeterministicId,
stopOrdersSignatureToDeterministicId,
} from './deterministic-ids';
it('txSignatureToDeterministicId Turns a known signature in to a known deterministic ID', () => {
const signature =
@@ -20,3 +24,24 @@ it('hexToString encodes a known good value as bytes', () => {
const res = hexToString(hex);
expect(res).toEqual([14, 221]);
});
describe('stopOrdersSignatureToDeterministicId', () => {
it('should return empty object if no signature is provided', () => {
const result = stopOrdersSignatureToDeterministicId();
expect(result.length).toEqual(0);
});
it('should return valid deterministic ids if a signature is provided', () => {
const signature = 'deadb33f';
const result = stopOrdersSignatureToDeterministicId(signature);
expect(result.length).toEqual(2);
expect(result[0]).toBe(
'4c45b67a8c08cbf1982883a75beaf309bf172461d04bd427623d6cd3d9ab0e91'
);
expect(result[1]).toBe(
'afe7509ff90d8f26339a0ab81e4d3e1fb6c4d44e94419aa5e13ae7659d894da1'
);
});
});
@@ -1,4 +1,6 @@
import type { components } from '../../../../types/explorer';
import { sha3_256 } from 'js-sha3';
type StopOrderSetup = components['schemas']['v1StopOrderSetup'];
/**
* Encodes a string as bytes
@@ -37,3 +39,58 @@ export function txSignatureToDeterministicId(signature: string): string {
return hash.hex();
}
/**
* Given a stop order signature string, returns the deterministic IDs of both potential
* Stop Orders. A stop order is not an order per se, but a trigger for an order. The order
* created by the stop order will have another ID based on the market event that hit the
* trigger, and as such is not deterministic.
*
* @param signature
* @returns string[]
*/
export function stopOrdersSignatureToDeterministicId(
signature?: string
): string[] {
if (!signature) {
return [];
}
const firstId = txSignatureToDeterministicId(signature);
return [firstId, txSignatureToDeterministicId(firstId)];
}
export type stopSignatures = {
risesAboveId: string | undefined;
fallsBelowId: string | undefined;
};
/**
* In 0.72.10 the way stop order IDs are determined is a little tricky. It will be stabilised
* in a future release.
* @param deterministicIds Output of stopORdersSignatureToDeterministicId
* @param risesAbove Stop order setup
* @param fallsBelow Stop order setup
* @returns Object containing the deterministic IDs of the stop orders
*/
export function getStopOrderIds(
deterministicIds: string[],
risesAbove: StopOrderSetup | undefined,
fallsBelow: StopOrderSetup | undefined
) {
if (risesAbove && fallsBelow) {
return {
risesAboveId: deterministicIds[0],
fallsBelowId: deterministicIds[1],
};
} else if (!fallsBelow && risesAbove) {
return {
risesAboveId: deterministicIds[0],
};
} else {
return {
fallsBelowId: deterministicIds[0] || undefined,
risesAboveId: deterministicIds[1] || undefined,
};
}
}
@@ -34,6 +34,7 @@ export type FilterOption =
| 'Protocol Upgrade'
| 'Register new Node'
| 'State Variable Proposal'
| 'Stop Orders Submission'
| 'Stop Orders Cancellation'
| 'Submit Oracle Data'
| 'Submit Order'
@@ -54,6 +55,7 @@ export const PrimaryFilterOptions: FilterOption[] = [
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Stop Orders Submission',
'Stop Orders Cancellation',
'Submit Oracle Data',
'Submit Order',
@@ -50,6 +50,25 @@ const displayString: StringMap = {
'Stop Orders Cancellation': 'Cancel stop',
};
export function getLabelForStopOrderType(
orderType: string,
command: components['schemas']['v1InputData']
): string {
if (command.stopOrdersSubmission) {
if (
command.stopOrdersSubmission.risesAbove &&
command.stopOrdersSubmission.fallsBelow
) {
return 'Stop ⇅';
} else if (command.stopOrdersSubmission.risesAbove) {
return 'Stop ↗';
} else if (command.stopOrdersSubmission.fallsBelow) {
return 'Stop ↘';
}
}
return 'Stop';
}
export function getLabelForOrderType(
orderType: string,
command: components['schemas']['v1InputData']
@@ -185,6 +204,9 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
} else if (type === 'Order' && command) {
type = getLabelForOrderType(orderType, command);
colours = 'text-white dark-text-white bg-vega-blue dark:bg-vega-blue';
} else if (type === 'Stop' && command) {
type = getLabelForStopOrderType(orderType, command);
colours = 'text-white dark-text-white bg-vega-blue dark:bg-vega-blue';
}
if (type === 'Vote on Proposal') {
@@ -55,7 +55,7 @@ export const TxsInfiniteList = ({
}
return (
<div className="overflow-scroll">
<div>
<table className={className} data-testid="transactions-list">
<thead>
<tr className="w-full mb-3 text-vega-dark-300 uppercase text-left">
@@ -1,4 +1,34 @@
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
fragment ExplorerOracleDataSource on OracleSpec {
...ExplorerOracleDataConnection
dataSourceSpec {
spec {
id
@@ -34,6 +64,7 @@ fragment ExplorerOracleDataSource on OracleSpec {
key {
name
type
numberDecimalPlaces
}
conditions {
value
@@ -49,38 +80,6 @@ fragment ExplorerOracleDataSource on OracleSpec {
}
}
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
pageInfo {
hasNextPage
}
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
query ExplorerOracleSpecs {
oracleSpecsConnection(pagination: { first: 50 }) {
pageInfo {
@@ -97,6 +96,5 @@ query ExplorerOracleSpecs {
query ExplorerOracleSpecById($id: ID!) {
oracleSpec(oracleSpecId: $id) {
...ExplorerOracleDataSource
...ExplorerOracleDataConnection
}
}
+38 -41
View File
@@ -3,24 +3,55 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } } };
export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleSpecsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } } } } | null> | null } | null };
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null };
export type ExplorerOracleSpecByIdQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null };
export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null };
export const ExplorerOracleDataConnectionFragmentDoc = gql`
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
`;
export const ExplorerOracleDataSourceFragmentDoc = gql`
fragment ExplorerOracleDataSource on OracleSpec {
...ExplorerOracleDataConnection
dataSourceSpec {
spec {
id
@@ -56,6 +87,7 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
key {
name
type
numberDecimalPlaces
}
conditions {
value
@@ -70,40 +102,7 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
}
}
}
`;
export const ExplorerOracleDataConnectionFragmentDoc = gql`
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
pageInfo {
hasNextPage
}
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
`;
${ExplorerOracleDataConnectionFragmentDoc}`;
export const ExplorerOracleSpecsDocument = gql`
query ExplorerOracleSpecs {
oracleSpecsConnection(pagination: {first: 50}) {
@@ -149,11 +148,9 @@ export const ExplorerOracleSpecByIdDocument = gql`
query ExplorerOracleSpecById($id: ID!) {
oracleSpec(oracleSpecId: $id) {
...ExplorerOracleDataSource
...ExplorerOracleDataConnection
}
}
${ExplorerOracleDataSourceFragmentDoc}
${ExplorerOracleDataConnectionFragmentDoc}`;
${ExplorerOracleDataSourceFragmentDoc}`;
/**
* __useExplorerOracleSpecByIdQuery__
@@ -1,23 +1,23 @@
import { render } from '@testing-library/react';
import { OracleData } from './oracle-data';
import type { ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
import { type ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
function renderComponent(data: ExplorerOracleDataConnectionFragment) {
type DataConnection = ExplorerOracleDataConnectionFragment['dataConnection'];
function renderComponent(
data: ExplorerOracleDataConnectionFragment['dataConnection']
) {
return <OracleData data={data} />;
}
describe('Oracle Data view', () => {
it('Renders nothing when data is null', () => {
const res = render(
renderComponent(null as unknown as ExplorerOracleDataConnectionFragment)
);
const res = render(renderComponent(null as unknown as DataConnection));
expect(res.container).toBeEmptyDOMElement();
});
it('Renders nothing when dataConnection is empty', () => {
const res = render(
renderComponent({} as ExplorerOracleDataConnectionFragment)
);
const res = render(renderComponent({} as DataConnection));
expect(res.container).toBeEmptyDOMElement();
});
it('Renders nothing when dataConnection has no edges', () => {
@@ -26,7 +26,7 @@ describe('Oracle Data view', () => {
dataConnection: {
edges: null,
},
} as ExplorerOracleDataConnectionFragment)
} as DataConnection)
);
expect(res.container).toBeEmptyDOMElement();
});
@@ -37,7 +37,7 @@ describe('Oracle Data view', () => {
dataConnection: {
edges: [],
},
} as unknown as ExplorerOracleDataConnectionFragment)
} as unknown as DataConnection)
);
expect(res.container).toBeEmptyDOMElement();
});
@@ -47,20 +47,18 @@ describe('Oracle Data view', () => {
it('Renders details component when there is data', () => {
const res = render(
renderComponent({
dataConnection: {
edges: [
{
node: {
externalData: {
data: {
broadcastAt: '2022-01-01',
},
edges: [
{
node: {
externalData: {
data: {
broadcastAt: '2022-01-01',
},
},
},
],
},
} as ExplorerOracleDataConnectionFragment)
},
],
} as DataConnection)
);
expect(res.getByText('Broadcast data')).toBeInTheDocument();
});
@@ -4,7 +4,7 @@ import filter from 'recursive-key-filter';
import type { ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
interface OracleDataTypeProps {
data: ExplorerOracleDataConnectionFragment;
data: ExplorerOracleDataConnectionFragment['dataConnection'];
}
/**
@@ -14,12 +14,7 @@ interface OracleDataTypeProps {
* that Does The Job, rather than because it's good.
*/
export function OracleData({ data }: OracleDataTypeProps) {
if (
!data ||
!data.dataConnection ||
!data.dataConnection.edges?.length ||
data.dataConnection.edges.length > 1
) {
if (!data || !data.edges?.length || data.edges.length > 1) {
return null;
}
@@ -27,7 +22,7 @@ export function OracleData({ data }: OracleDataTypeProps) {
<details data-testid="oracle-data">
<summary>{t('Broadcast data')}</summary>
<ul>
{data.dataConnection.edges.map((d) => {
{data.edges.map((d) => {
if (!d) {
return null;
}
@@ -27,8 +27,8 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
const m = markets.find((m) => {
const p = m.tradableInstrument.instrument.product;
if (
p.dataSourceSpecForSettlementData.id === id ||
p.dataSourceSpecForTradingTermination.id === id
p?.dataSourceSpecForSettlementData?.id === id ||
p?.dataSourceSpecForTradingTermination?.id === id
) {
return true;
}
@@ -36,15 +36,9 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
});
if (m && m.id) {
const type =
id ===
m.tradableInstrument.instrument.product.dataSourceSpecForSettlementData
.id
? 'Settlement for'
: 'Termination for';
return (
<TableRow modifier="bordered">
<TableHeader scope="row">{type}</TableHeader>
<TableHeader scope="row">{getLabel(id, m)}</TableHeader>
<TableCell modifier="bordered" data-testid={`m-${m.id}`}>
<MarketLink id={m.id} />
</TableCell>
@@ -61,3 +55,14 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
</TableRow>
);
}
export function getLabel(
id: string,
m: ExplorerOracleForMarketsMarketFragment | null
): string {
const settlementId =
m?.tradableInstrument?.instrument?.product?.dataSourceSpecForSettlementData
?.id || null;
return id === settlementId ? 'Settlement for' : 'Termination for';
}
@@ -22,7 +22,7 @@ export type SourceType =
interface OracleDetailsProps {
id: string;
dataSource: ExplorerOracleDataSourceFragment;
dataConnection?: ExplorerOracleDataConnectionFragment;
dataConnection: ExplorerOracleDataConnectionFragment['dataConnection'];
// Defaults to false. Hides the count of 'broadcasts' this oracle has seen
showBroadcasts?: boolean;
}
@@ -41,8 +41,7 @@ export const OracleDetails = ({
showBroadcasts = false,
}: OracleDetailsProps) => {
const sourceType = dataSource.dataSourceSpec.spec.data.sourceType;
const reportsCount: number =
dataConnection?.dataConnection.edges?.length || 0;
const reportsCount: number = dataConnection.edges?.length || 0;
return (
<div>
@@ -34,11 +34,15 @@ const Oracles = () => {
if (!id) {
return null;
}
const dataConnection = o?.node.dataConnection;
return (
<div id={id} key={id} className="mb-10">
<OracleDetails
id={id}
dataSource={o?.node}
dataConnection={dataConnection}
showBroadcasts={false}
/>
<details>
@@ -39,7 +39,7 @@ export const Oracle = () => {
<OracleDetails
id={id || ''}
dataSource={data?.oracleSpec}
dataConnection={data?.oracleSpec}
dataConnection={data?.oracleSpec.dataConnection}
showBroadcasts={true}
/>
<details>
+5 -1
View File
@@ -920,6 +920,8 @@ export interface components {
* - ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: Per asset reward account for fees received by liquidity providers
* - ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: Per asset reward account for market proposers when the market goes above some trading threshold
* - ACCOUNT_TYPE_HOLDING: Per asset account for holding in-flight unfilled orders' funds
* - ACCOUNT_TYPE_LP_LIQUIDITY_FEES: Network controlled liquidity provider's account, per market, to hold accrued liquidity fees.
* - ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: Network controlled liquidity fees bonus distribution account, per market.
* @default ACCOUNT_TYPE_UNSPECIFIED
* @enum {string}
*/
@@ -941,7 +943,9 @@ export interface components {
| 'ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES'
| 'ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES'
| 'ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS'
| 'ACCOUNT_TYPE_HOLDING';
| 'ACCOUNT_TYPE_HOLDING'
| 'ACCOUNT_TYPE_LP_LIQUIDITY_FEES'
| 'ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION';
/** Vega representation of an external asset */
readonly vegaAssetDetails: {
/** @description Vega built-in asset. */
@@ -11,6 +11,9 @@ import {
getDateFormatForSpecifiedDays,
getProposalFromTitle,
getProposalInformationFromTable,
goToMakeNewProposal,
governanceProposalType,
longProposalDescription,
proposalChangeType,
submitUniqueRawProposal,
validateProposalDetailsDiff,
@@ -43,7 +46,6 @@ const proposalDetailsTitle = 'proposal-title';
const proposalDetailsDescription = 'proposal-description';
const openProposals = 'open-proposals';
const viewProposalButton = 'view-proposal-btn';
const proposalDescriptionToggle = 'proposal-description-toggle';
const voteBreakdownToggle = 'vote-breakdown-toggle';
const proposalTermsToggle = 'proposal-json-toggle';
const marketDataToggle = 'proposal-market-data-toggle';
@@ -71,10 +73,13 @@ describe(
// 3001-VOTE-050 3001-VOTE-054 3001-VOTE-055 3002-PROP-019
it('Newly created raw proposal details - shows proposal title and full description', function () {
const proposalDescription =
'I propose that everyone evaluate the following IPFS document and vote Yes if they agree. bafybeigwwctpv37xdcwacqxvekr6e4kaemqsrv34em6glkbiceo3fcy4si';
const proposalDetails = longProposalDescription;
createRawProposal();
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({
proposalTitle: 'raw proposal with long description',
proposalDescription: proposalDetails,
});
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
cy.getByTestId(openProposals).within(() => {
getProposalFromTitle(rawProposal.rationale.title).within(() => {
@@ -85,12 +90,17 @@ describe(
'contain.text',
rawProposal.rationale.title
);
cy.getByTestId(proposalDescriptionToggle).click();
cy.getByTestId('proposal-description-toggle');
cy.getByTestId(proposalDetailsDescription)
.find('p')
.should('have.text', proposalDescription);
});
cy.getByTestId(proposalDetailsDescription).within(() => {
cy.get('p').should('not.have.text', 'Hyperlink text');
cy.getByTestId('show-more-btn').click();
cy.get('p')
.invoke('text')
.should('have.have.length', 2194) // Full description is displayed
.and('contain', 'Hyperlink text');
cy.get('a').should('have.attr', 'href');
});
// 3001-VOTE-008
getProposalInformationFromTable('ID')
.invoke('text')
@@ -361,34 +371,6 @@ describe(
stakingPageDisassociateAllTokens();
});
it('Error message should be displayed if error returned from wallet when voting', function () {
const errorMsg =
'Application error: party has already submitted the maximum number of transactions of this type per epoch (3)';
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.getByTestId(viewProposalButton).click()
);
cy.intercept('POST', '/api/v2/requests', {
jsonrpc: '2.0',
error: {
code: 2001,
message: 'Application error',
data: 'party has already submitted the maximum number of transactions of this type per epoch (3)',
},
id: '-PK5EGmErnjLhAmzMeclC',
});
cy.contains('Vote breakdown').should('be.visible', { timeout: 10000 });
cy.getByTestId('vote-buttons').contains('for').click();
cy.getByTestId('dialog-title').should(
'have.text',
'Transaction failed'
);
cy.getByTestId('Error').should('have.text', errorMsg);
});
});
it('Able to see successor market details with new and updated values', function () {
cy.createMarket();
cy.reload();
@@ -425,7 +407,8 @@ describe(
// 3003-PMAN-011 3003-PMAN-012
cy.getByTestId(marketDataToggle).click();
cy.getByTestId('proposal-market-data').within(() => {
cy.contains('Key details').click();
// Assert that all toggles are removed
cy.getByTestId('accordion-toggle').should('not.exist');
validateProposalDetailsDiff(
'Name',
proposalChangeType.UPDATED,
@@ -449,7 +432,6 @@ describe(
'Opening auction'
);
cy.contains('Instrument').click();
validateProposalDetailsDiff(
'Market Name',
proposalChangeType.UPDATED,
@@ -457,7 +439,6 @@ describe(
'Test market 1'
);
cy.contains('Metadata').click();
validateProposalDetailsDiff(
'Sector',
proposalChangeType.UPDATED,
@@ -274,3 +274,6 @@ export enum proposalChangeType {
UPDATED = 'Updated',
ADDED = 'Added',
}
export const longProposalDescription =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam bibendum orci augue, vel imperdiet augue ultrices sed. In hac habitasse platea dictumst. Sed eget elit vitae nisl tincidunt faucibus. Donec pellentesque mauris nec viverra blandit. Aenean eros diam, tempor eu luctus nec, rhoncus non massa. Mauris libero diam, mattis et enim ut, lobortis pharetra elit. Phasellus vel metus accumsan, rhoncus tellus finibus, blandit mi. In sollicitudin ex ac tortor ornare interdum. Sed est ipsum, vestibulum eget dolor vel, porta luctus elit. Fusce justo nibh, placerat eget sollicitudin eleifend, rhoncus id lorem. Fusce vitae magna vel urna faucibus accumsan quis id purus.\nPraesent convallis dolor sed ante ultricies tempor. Proin sed risus ut libero euismod semper. Duis quis quam sed lacus viverra blandit vel scelerisque diam. Donec interdum, ipsum eget imperdiet ornare, risus augue faucibus lectus, ullamcorper scelerisque erat sapien in purus. Nunc molestie tincidunt felis dignissim vestibulum. Quisque quis ornare enim, non dignissim lectus. Mauris mollis, massa ut maximus consectetur, sem mi lobortis quam, vel malesuada eros tortor nec ex. Cras ac nunc sed erat malesuada varius a quis nulla. Curabitur cursus nec sem sit amet aliquet. Ut tristique tortor neque, a dignissim lectus dictum vel. Praesent sollicitudin bibendum vulputate.\nAenean bibendum tristique diam laoreet posuere. Curabitur ornare lectus ut diam ultricies, ut sodales eros lacinia. Maecenas mauris turpis, gravida non arcu ac, interdum auctor sapien. Vestibulum sed tortor quam. Interdum et malesuada fames ac ante ipsum primis in faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec finibus pulvinar magna, non laoreet lectus molestie nec. Vestibulum tempus mattis vehicula. Praesent in orci lectus. In commodo sollicitudin lacus, et lobortis eros placerat vitae. Proin mi libero, feugiat id pretium posuere, rhoncus ut augue. Cras massa tortor, rutrum sed ex vitae, posuere pretium augue. Donec pellentesque suscipit dignissim. Vivamus convallis a odio vitae sodales. Nullam non eleifend mauris, sed iaculis lectus. Cras facilisis justo at ante.\n[Hyperlink text](https://dweb.link/ipfs/bafybeigwwctpv37xdcwacqxvekr6e4kaemqsrv34em6glkbiceo3fcy4si)';
+1
View File
@@ -29,3 +29,4 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1
View File
@@ -30,3 +30,4 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+1
View File
@@ -22,3 +22,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1
View File
@@ -22,3 +22,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+1
View File
@@ -21,3 +21,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+1
View File
@@ -18,3 +18,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1
View File
@@ -23,3 +23,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1
View File
@@ -20,3 +20,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+10 -1
View File
@@ -1,17 +1,26 @@
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
const urlParams = new URLSearchParams(window.location.search);
export const injected = new InjectedConnector();
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
};
@@ -1,44 +1,27 @@
import ReactMarkdown from 'react-markdown';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { RoundedWrapper, ShowMore } from '@vegaprotocol/ui-toolkit';
export const ProposalDescription = ({
description,
}: {
description: string;
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
return (
<section data-testid="proposal-description">
<CollapsibleToggle
toggleState={showDescription}
setToggleState={setShowDescription}
dataTestId={'proposal-description-toggle'}
>
<SubHeading title={t('proposalDescription')} />
</CollapsibleToggle>
{showDescription && (
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
<div className="p-2">
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
</RoundedWrapper>
)}
</section>
);
};
}) => (
<section data-testid="proposal-description">
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
<div className="p-2">
<ShowMore>
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</ShowMore>
</div>
</RoundedWrapper>
</section>
);
@@ -15,8 +15,6 @@ import {
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
@@ -43,6 +41,9 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
})
);
const marketDataHeaderStyles =
'font-alpha calt text-base border-b border-vega-dark-200 mt-2 py-2';
export const ProposalMarketData = ({
marketData,
parentMarketData,
@@ -76,6 +77,14 @@ export const ProposalMarketData = ({
parentTerminationData !== undefined &&
isEqual(terminationData, parentTerminationData);
const showParentPriceMonitoringBounds =
parentMarketData?.priceMonitoringSettings?.parameters?.triggers !==
undefined &&
!isEqual(
marketData.priceMonitoringSettings?.parameters?.triggers,
parentMarketData?.priceMonitoringSettings?.parameters?.triggers
);
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
@@ -108,164 +117,141 @@ export const ProposalMarketData = ({
</Button>
</div>
<div className="mb-10">
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
<h2 className={marketDataHeaderStyles}>{t('Key details')}</h2>
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Instrument')}</h2>
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<>
<h2 className={marketDataHeaderStyles}>{t('Oracle')}</h2>
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual ? undefined : parentMarketData
}
/>
</>
) : (
<>
<h2 className={marketDataHeaderStyles}>
{t('Settlement Oracle')}
</h2>
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual ? undefined : parentMarketData
}
/>
) : (
<>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual
? undefined
: parentMarketData
}
/>
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
<h2 className={marketDataHeaderStyles}>
{t('Termination Oracle')}
</h2>
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual ? undefined : parentMarketData
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<h2 className={marketDataHeaderStyles}>{t('Settlement assets')}</h2>
<SettlementAssetInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Metadata')}</h2>
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk model')}</h2>
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk factors')}</h2>
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
{showParentPriceMonitoringBounds &&
(
parentMarketData?.priceMonitoringSettings?.parameters
?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<>
<h2 className={marketDataHeaderStyles}>
{t(`Parent price monitoring bounds ${triggerIndex + 1}`)}
</h2>
<div className="text-vega-dark-300 line-through">
<PriceMonitoringBoundsInfoPanel
market={marketData}
parentMarket={parentMarketData}
market={parentMarketData}
triggerIndex={triggerIndex}
/>
}
/>
</div>
</>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
</Accordion>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<>
<h2 className={marketDataHeaderStyles}>
{t(`Price monitoring bounds ${triggerIndex + 1}`)}
</h2>
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
/>
</>
))}
<h2 className={marketDataHeaderStyles}>
{t('Liquidity monitoring parameters')}
</h2>
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>
{t('Liquidity price range')}
</h2>
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
</div>
</>
)}
@@ -1,218 +0,0 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types';
const rowSelector =
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode =
'[col-id="tradableInstrument.instrument.code"] [data-testid="market-code"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/#/markets/all');
});
});
it('can see table headers', () => {
cy.wait('@Markets');
cy.wait('@MarketsData');
const headers = [
'Market',
'Description',
'Trading mode',
'Status',
'Successor market',
'Best bid',
'Best offer',
'Mark price',
'Settlement asset',
'',
];
cy.getByTestId('tab-open-markets').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('markets tab should be rendered properly', () => {
cy.get('[data-testid="Open markets"]').should(
'have.attr',
'data-state',
'active'
);
cy.get('[data-testid="Proposed markets"]').should(
'have.attr',
'data-state',
'inactive'
);
cy.get('[data-testid="Closed markets"]').should(
'have.attr',
'data-state',
'inactive'
);
});
it('renders markets correctly', () => {
// 6001-MARK-035
cy.get(rowSelector)
.first()
.find(colInstrumentCode)
.should('have.text', 'SOLUSD');
// 6001-MARK-073
cy.get(rowSelector)
.first()
.find('[title="Future"]')
.should('have.text', 'Futr');
// 6001-MARK-036
cy.get(rowSelector)
.first()
.find('[col-id="tradableInstrument.instrument.name"]')
.should('have.text', 'SUSPENDED MARKET');
// 6001-MARK-037
cy.get(rowSelector)
.first()
.find('[col-id="tradingMode"]')
.should('have.text', 'Continuous');
// 6001-MARK-038
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', 'Active');
// 6001-MARK-039
cy.get(rowSelector)
.first()
.find('[col-id="data.bestBidPrice"]')
.should('have.text', '0.00');
// 6001-MARK-040
cy.get(rowSelector)
.first()
.find('[col-id="data.bestOfferPrice"]')
.should('have.text', '0.00');
// 6001-MARK-041
cy.get(rowSelector)
.first()
.find('[col-id="data.markPrice"]')
.should('have.text', '84.41');
// 6001-MARK-042
cy.get(rowSelector)
.first()
.find(
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"]'
)
.should('have.text', 'XYZalpha');
// 6001-MARK-043
cy.get(rowSelector)
.first()
.find(
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"] button'
)
.click();
cy.getByTestId('dialog-title').should('have.text', 'Asset details - tEURO');
cy.getByTestId('close-asset-details-dialog').click();
});
it('can open row actions', () => {
// 6001-MARK-044
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button')
.click();
// 6001-MARK-045
const dropdownContent = '[data-testid="market-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
// Cannot click the copy button as it falls back to window.prompt, blocking the test.
.should('have.text', 'Copy Market ID');
// 6001-MARK-046
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(1)
.find('a')
.then(($el) => {
const href = $el.attr('href');
expect(/\/markets\/market-1/.test(href || '')).to.equal(true);
})
.should('have.text', 'View on Explorer');
// 6001-MARK-047
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(2)
.should('have.text', 'View settlement asset details');
cy.getByTestId('market-actions-content').click();
});
it('able to open and sort full market list - market page', () => {
// 6001-MARK-064
const ExpectedSortedMarkets = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
cy.get('[data-testid="Open markets"]').click({ force: true });
cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all');
cy.contains('AAPL.MF21').should('be.visible');
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
for (let i = 0; i < ExpectedSortedMarkets.length; i++) {
cy.get(`[row-index=${i}]`)
.find(colInstrumentCode)
.should('have.text', ExpectedSortedMarkets[i]);
}
});
it('can drag and drop columns', () => {
// 6001-MARK-065
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get(colInstrumentCode)
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
cy.get(colInstrumentCode).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no open markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const markets: MarketsQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Markets', markets);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
});
it.skip('can see no markets message', () => {
// 6001-MARK-048
cy.getByTestId('tab-open-markets').should('contain.text', 'No markets');
});
});
@@ -1,239 +0,0 @@
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"] [data-testid="market-code"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see table headers', () => {
const headers = [
'Market',
'Description',
'Settlement asset',
'State',
'Parent market',
'Voting',
'Closing date',
'Enactment date',
'',
];
cy.getByTestId('tab-proposed-markets').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('renders markets correctly', () => {
// 6001-MARK-049
cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD');
// 6001-MARK-050
cy.get(rowSelector)
.first()
.find('[col-id="description"]')
.should('have.text', 'ETHUSD');
// 6001-MARK-074
cy.get(rowSelector)
.first()
.find('[title="Future"]')
.should('have.text', 'Futr');
// 6001-MARK-051
cy.get(rowSelector)
.first()
.find('[col-id="asset"]')
.should('have.text', 'tDAI TEST');
// 6001-MARK-052
// 6001-MARK-053
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', 'Open');
// 6001-MARK-054
cy.get(rowSelector)
.first()
.find('[col-id="voting"]')
.should('have.text', '');
// 6001-MARK-056
cy.get(rowSelector)
.first()
.find('[col-id="closing-date"]')
.should('not.be.empty');
// 6001-MARK-057
cy.get(rowSelector)
.first()
.find('[col-id="enactment-date"]')
.should('not.be.empty');
});
it('can open row actions', () => {
// 6001-MARK-058
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="proposal-actions"]')
.first()
.find('button')
.click();
const dropdownContent = '[data-testid="proposal-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
// 6001-MARK-059
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
.find('a')
.should('have.text', 'View proposal')
.and(
'have.attr',
'href',
`${Cypress.env(
'VEGA_TOKEN_URL'
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
);
});
// 6001-MARK-060
it('can see proposed market link', () => {
cy.getByTestId('tab-proposed-markets')
.find('[data-testid="external-link"]')
.should('have.length', 11)
.last()
.should('have.text', 'Propose a new market')
.and(
'have.attr',
'href',
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/propose/new-market`
);
});
it('proposed markets tab should be sorted properly', () => {
// 6001-MARK-062
cy.get('[data-testid="Proposed markets"]').click({ force: true });
const marketColDefault = [
'ETHUSD',
'LINKUSD',
'ETHUSD',
'ETHDAI.MF21',
'AAPL.MF21',
'BTCUSD.MF21',
'TSLA.QM21',
'AAVEDAI.MF21',
'ETHBTC.QM21',
'UNIDAI.MF21',
];
const marketColAsc = [
'AAPL.MF21',
'AAVEDAI.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'ETHDAI.MF21',
'ETHUSD',
'ETHUSD',
'LINKUSD',
'TSLA.QM21',
'UNIDAI.MF21',
];
const marketColDesc = [
'UNIDAI.MF21',
'TSLA.QM21',
'LINKUSD',
'ETHUSD',
'ETHUSD',
'ETHDAI.MF21',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAVEDAI.MF21',
'AAPL.MF21',
];
checkSorting(
'market',
marketColDefault,
marketColAsc,
marketColDesc,
' [data-testid="market-code"]'
);
const stateColDefault = [
'Open',
'Passed',
'Waiting for Node Vote',
'Open',
'Passed',
'Open',
'Passed',
'Open',
'Waiting for Node Vote',
'Open',
];
const stateColAsc = [
'Open',
'Open',
'Open',
'Open',
'Open',
'Passed',
'Passed',
'Passed',
'Waiting for Node Vote',
'Waiting for Node Vote',
];
const stateColDesc = [
'Waiting for Node Vote',
'Waiting for Node Vote',
'Passed',
'Passed',
'Passed',
'Open',
'Open',
'Open',
'Open',
'Open',
];
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
});
it('can drag and drop columns', () => {
// 6001-MARK-063
cy.get(colMarketId).realMouseDown().realMouseMove(700, 15).realMouseUp();
cy.get(colMarketId).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const proposal: ProposalsListQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ProposalsList', proposal);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
});
it.skip('can see no markets message', () => {
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
// 6001-MARK-061
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
});
});
@@ -1,110 +0,0 @@
import * as Schema from '@vegaprotocol/types';
import { testOrderSubmission } from '../support/order-validation';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe.skip('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.setVegaWallet();
});
it('successfully places market buy order', () => {
// 7002-SORD-010
// 0003-WTXN-012
// 0003-WTXN-003
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
postOnly: false,
reduceOnly: false,
size: '100',
};
createOrder(order);
testOrderSubmission(order);
});
it('successfully places market sell order', () => {
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
size: '100',
postOnly: false,
reduceOnly: false,
};
createOrder(order);
testOrderSubmission(order);
});
it('successfully places limit buy order', () => {
// 7002-SORD-017
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
size: '100',
postOnly: false,
reduceOnly: false,
price: '200',
};
createOrder(order);
testOrderSubmission(order, { price: '20000000' });
});
it('successfully places limit sell order', () => {
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GFN,
size: '100',
postOnly: false,
reduceOnly: false,
price: '50000',
};
createOrder(order);
testOrderSubmission(order, { price: '5000000000' });
});
it('successfully places GTT limit buy order', () => {
cy.mockVegaWalletTransaction();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
size: '100',
price: '1.00',
expiresAt: expiresAt.toISOString().substring(0, 16),
postOnly: false,
reduceOnly: false,
};
createOrder(order);
testOrderSubmission(order, {
price: '100000',
expiresAt:
new Date(order.expiresAt as string).getTime().toString() + '000000',
postOnly: false,
reduceOnly: false,
});
});
});
@@ -64,7 +64,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-price-limit').should(
cy.getByTestId('deal-ticket-error-message-price').should(
'have.text',
'Price accepts up to 5 decimal places'
);
@@ -87,7 +87,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1.234');
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('deal-ticket-error-message-size-market').should(
cy.getByTestId('deal-ticket-error-message-size').should(
'have.text',
'Size must be whole numbers for this market'
);
@@ -96,7 +96,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must warn if order size is set to 0', function () {
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('deal-ticket-error-message-size-market').should(
cy.getByTestId('deal-ticket-error-message-size').should(
'have.text',
'Size cannot be lower than 1'
);
@@ -1,59 +1,25 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { ledgerEntriesQuery } from '@vegaprotocol/mock';
import { partyAssetsQuery } from '@vegaprotocol/mock';
describe('Portfolio page', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'LedgerEntries', ledgerEntriesQuery());
aliasGQLQuery(req, 'PartyAssets', partyAssetsQuery());
});
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
});
describe('Ledger entries', () => {
it('List should be properly rendered', () => {
it('Download form should be properly rendered', () => {
// 7007-LEEN-001
cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click();
const headers = [
'Sender',
'Account type',
'Market',
'Receiver',
'Account type',
'Market',
'Transfer type',
'Quantity',
'Asset',
'Sender account balance',
'Receiver account balance',
'Vega time',
];
cy.getByTestId('tab-ledger-entries').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
.getByTestId('ledger-download-button')
.should('be.visible');
});
cy.get(
'[data-testid="tab-ledger-entries"] .ag-center-cols-container .ag-row'
).should('have.length', ledgerEntriesQuery().ledgerEntries.edges.length);
});
it('account filters should be callable', () => {
cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click();
cy.get('[role="columnheader"][col-id="fromAccountType"]').realHover();
cy.get(
'[role="columnheader"][col-id="fromAccountType"] .ag-header-cell-menu-button'
).click();
cy.get('fieldset.ag-simple-filter-body-wrapper')
.should('be.visible')
.within((fields) => {
cy.wrap(fields).find('label').should('have.length', 18);
});
cy.getByTestId('"Ledger entries"').click();
cy.get('fieldset.ag-simple-filter-body-wrapper').should('not.exist');
});
});
});
+1
View File
@@ -19,6 +19,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+1
View File
@@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
+1
View File
@@ -21,6 +21,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
+1
View File
@@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+1
View File
@@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+1
View File
@@ -21,3 +21,4 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
+1
View File
@@ -22,6 +22,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+1
View File
@@ -23,6 +23,7 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
@@ -1,23 +1,30 @@
import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import { LedgerManager } from '@vegaprotocol/ledger';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { LedgerExportForm } from '@vegaprotocol/ledger';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useEnvironment } from '@vegaprotocol/environment';
import type { PartyAssetFieldsFragment } from '@vegaprotocol/assets';
import { usePartyAssetsQuery } from '@vegaprotocol/assets';
export const LedgerContainer = () => {
const VEGA_URL = useEnvironment((store) => store.VEGA_URL);
const { pubKey } = useVegaWallet();
const gridStore = useLedgerStore((store) => store.gridStore);
const updateGridStore = useLedgerStore((store) => store.updateGridStore);
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
updateGridStore(colState);
const { data, loading } = usePartyAssetsQuery({
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
if (!pubKey) {
return (
<Splash>
@@ -26,11 +33,31 @@ export const LedgerContainer = () => {
);
}
return <LedgerManager partyId={pubKey} gridProps={gridStoreCallbacks} />;
};
if (!VEGA_URL) {
return (
<Splash>
<p>{t('Environment not configured')}</p>
</Splash>
);
}
const useLedgerStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_ledger_store',
})
);
if (loading) {
return (
<div className="relative flex items-center justify-center w-full h-full">
<Loader />
</div>
);
}
if (!Object.keys(assets).length) {
return (
<Splash>
<p>{t('No ledger entries to export')}</p>
</Splash>
);
}
return (
<LedgerExportForm partyId={pubKey} vegaUrl={VEGA_URL} assets={assets} />
);
};
@@ -36,6 +36,6 @@ export const StopOrdersContainer = () => {
const useStopOrdersStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_fills_store',
name: 'vega_stop_orders_store',
})
);
@@ -105,38 +105,29 @@ export const GetStarted = ({ lead }: Props) => {
{lead && <h2>{lead}</h2>}
<h3 className="text-lg">{t('Get started')}</h3>
<div>
<ul className="list-inside -ml-5" role="list">
<li className="flex">
<div className="w-5">
{currentStep > OnboardingStep.ONBOARDING_WALLET_STEP && (
<VegaIcon name={VegaIconNames.TICK} size={20} />
)}
</div>
<div className="ml-1">1. {t('Get a Vega wallet')}</div>
</li>
<li className="flex">
<div className="w-5">
{(currentStep > OnboardingStep.ONBOARDING_CONNECT_STEP ||
pubKey) && <VegaIcon name={VegaIconNames.TICK} size={20} />}
</div>
<div className="ml-1">2. {t('Connect')}</div>
</li>
<li className="flex">
<div className="w-5">
{currentStep > OnboardingStep.ONBOARDING_DEPOSIT_STEP && (
<VegaIcon name={VegaIconNames.TICK} size={20} />
)}
</div>
<div className="ml-1">3. {t('Deposit funds')}</div>
</li>
<li className="flex">
<div className="w-5">
{currentStep > OnboardingStep.ONBOARDING_ORDER_STEP && (
<VegaIcon name={VegaIconNames.TICK} size={20} />
)}
</div>
<div className="ml-1">4. {t('Open a position')}</div>
</li>
<ul className="list-none">
<Step
step={1}
text={t('Get a Vega wallet')}
complete={currentStep > OnboardingStep.ONBOARDING_WALLET_STEP}
/>
<Step
step={2}
text={t('Connect')}
complete={Boolean(
currentStep > OnboardingStep.ONBOARDING_CONNECT_STEP || pubKey
)}
/>
<Step
step={3}
text={t('Deposit funds')}
complete={currentStep > OnboardingStep.ONBOARDING_DEPOSIT_STEP}
/>
<Step
step={4}
text={t('Open a position')}
complete={currentStep > OnboardingStep.ONBOARDING_ORDER_STEP}
/>
</ul>
</div>
<div>
@@ -165,7 +156,7 @@ export const GetStarted = ({ lead }: Props) => {
if (!pubKey) {
return (
<div className={wrapperClasses}>
<p className="text-sm mb-1">
<p className="mb-1 text-sm">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
@@ -186,3 +177,34 @@ export const GetStarted = ({ lead }: Props) => {
return null;
};
const Step = ({
step,
text,
complete,
}: {
step: number;
text: string;
complete: boolean;
}) => {
return (
<li
className={classNames('flex', {
'text-vega-clight-200 dark:text-vega-cdark-200': complete,
})}
>
<div className="flex justify-center w-5">
{complete ? <Tick /> : <span>{step}.</span>}
</div>
<div className="ml-1">{text}</div>
</li>
);
};
const Tick = () => {
return (
<span className="relative right-[2px]">
<VegaIcon name={VegaIconNames.TICK} size={18} />
</span>
);
};
+9
View File
@@ -1,7 +1,10 @@
import { ENV } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
export const jsonRpc = new JsonRpcConnector();
@@ -15,8 +18,14 @@ if (typeof window !== 'undefined') {
view = new ViewConnector();
}
export const snap = new SnapConnector(
ENV.VEGA_URL ? new URL(ENV.VEGA_URL).origin : undefined,
DEFAULT_SNAP_ID
);
export const Connectors = {
injected,
jsonRpc,
view,
snap,
};
+21 -1
View File
@@ -15,6 +15,10 @@ body,
@apply h-full;
}
.font-mono {
@apply tracking-tighter;
}
.text-default {
@apply text-vega-clight-50 dark:text-vega-cdark-50;
}
@@ -60,6 +64,10 @@ html.dark {
html [data-theme='dark'],
html [data-theme='light'] {
/* fonts */
--pennant-font-family-base: theme(fontFamily.alpha);
--pennant-font-family-monospace: theme(fontFamily.mono);
/* sell candles only use stroke as the candle is solid (without border) */
--pennant-color-sell-stroke: theme(colors.market.red.DEFAULT);
@@ -147,7 +155,7 @@ html [data-theme='dark'] {
}
.vega-ag-grid .ag-header-row {
@apply font-alpha font-normal;
@apply font-normal font-alpha;
}
/* Light variables */
@@ -209,3 +217,15 @@ html [data-theme='dark'] {
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
background-color: #999;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
+23
View File
@@ -24,3 +24,26 @@ query Assets {
}
}
}
fragment PartyAssetFields on Asset {
id
name
symbol
status
}
query PartyAssets($partyId: ID!) {
party(id: $partyId) {
id
accountsConnection {
edges {
node {
type
asset {
...PartyAssetFields
}
}
}
}
}
}
+63 -1
View File
@@ -10,6 +10,15 @@ export type AssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type AssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, quantum: string, status: Types.AssetStatus, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string, lifetimeLimit: string, withdrawThreshold: string } } } | null> | null } | null };
export type PartyAssetFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, status: Types.AssetStatus };
export type PartyAssetsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PartyAssetsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, asset: { __typename?: 'Asset', id: string, name: string, symbol: string, status: Types.AssetStatus } } } | null> | null } | null } | null };
export const AssetListFieldsFragmentDoc = gql`
fragment AssetListFields on Asset {
id
@@ -28,6 +37,14 @@ export const AssetListFieldsFragmentDoc = gql`
status
}
`;
export const PartyAssetFieldsFragmentDoc = gql`
fragment PartyAssetFields on Asset {
id
name
symbol
status
}
`;
export const AssetsDocument = gql`
query Assets {
assetsConnection {
@@ -65,4 +82,49 @@ export function useAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Ass
}
export type AssetsQueryHookResult = ReturnType<typeof useAssetsQuery>;
export type AssetsLazyQueryHookResult = ReturnType<typeof useAssetsLazyQuery>;
export type AssetsQueryResult = Apollo.QueryResult<AssetsQuery, AssetsQueryVariables>;
export type AssetsQueryResult = Apollo.QueryResult<AssetsQuery, AssetsQueryVariables>;
export const PartyAssetsDocument = gql`
query PartyAssets($partyId: ID!) {
party(id: $partyId) {
id
accountsConnection {
edges {
node {
type
asset {
...PartyAssetFields
}
}
}
}
}
}
${PartyAssetFieldsFragmentDoc}`;
/**
* __usePartyAssetsQuery__
*
* To run a query within a React component, call `usePartyAssetsQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = usePartyAssetsQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function usePartyAssetsQuery(baseOptions: Apollo.QueryHookOptions<PartyAssetsQuery, PartyAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(PartyAssetsDocument, options);
}
export function usePartyAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PartyAssetsQuery, PartyAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(PartyAssetsDocument, options);
}
export type PartyAssetsQueryHookResult = ReturnType<typeof usePartyAssetsQuery>;
export type PartyAssetsLazyQueryHookResult = ReturnType<typeof usePartyAssetsLazyQuery>;
export type PartyAssetsQueryResult = Apollo.QueryResult<PartyAssetsQuery, PartyAssetsQueryVariables>;
+47
View File
@@ -0,0 +1,47 @@
import merge from 'lodash/merge';
import type { PartyAssetsQuery } from './__generated__/Assets';
import * as Types from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
export const partyAssetsQuery = (
override?: PartialDeep<PartyAssetsQuery>
): PartyAssetsQuery => {
const defaultAssets: PartyAssetsQuery = {
party: {
__typename: 'Party',
id: 'partyId',
accountsConnection: {
edges: partyAccountFields.map((node) => ({
__typename: 'AccountEdge',
node,
})),
},
},
};
return merge(defaultAssets, override);
};
const partyAccountFields = [
{
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
asset: {
__typename: 'Asset',
id: 'asset-id',
symbol: 'tEURO',
name: 'Euro',
status: Types.AssetStatus.STATUS_ENABLED,
},
},
{
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
asset: {
__typename: 'Asset',
id: 'asset-id-2',
symbol: 'tDAI',
name: 'DAI',
status: Types.AssetStatus.STATUS_ENABLED,
},
},
] as const;
+1 -1
View File
@@ -2,6 +2,7 @@
export * from '../accounts/src/lib/accounts.mock';
export * from '../assets/src/lib/asset.mock';
export * from '../assets/src/lib/assets.mock';
export * from '../assets/src/lib/party-assets.mock';
export * from '../candles-chart/src/lib/candles.mock';
export * from '../candles-chart/src/lib/chart.mock';
export * from '../deal-ticket/src/hooks/estimate-order.mock';
@@ -10,7 +11,6 @@ export * from '../environment/src/utils/node.mock';
export * from '../environment/src/components/node-guard/node-guard.mock';
export * from '../fills/src/lib/fills.mock';
export * from '../proposals/src/lib/proposals-data-provider/proposals.mock';
export * from '../ledger/src/lib/ledger-entries.mock';
export * from '../market-depth/src/lib/market-depth.mock';
export * from '../markets/src/lib/components/market-info/market-info.mock';
export * from '../markets/src/lib/market-candles.mock';
@@ -153,8 +153,10 @@ export function waitForProposal(id: string): Promise<{ id: string }> {
try {
const res = await getProposal(id);
if (
res.proposal !== null &&
res.proposal.state === Schema.ProposalState.STATE_OPEN
(res.proposal !== null &&
res.proposal.state === Schema.ProposalState.STATE_OPEN) ||
res.proposal.state ===
Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE
) {
clearInterval(interval);
resolve(res.proposal);
@@ -1,39 +0,0 @@
import type { Control } from 'react-hook-form';
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
import * as Schema from '@vegaprotocol/types';
import type { OrderFormValues } from '../../hooks/use-form-values';
export interface DealTicketAmountProps {
control: Control<OrderFormValues>;
type: Schema.OrderType;
marketData: StaticMarketData;
marketPrice?: string;
market: Market;
sizeError?: string;
priceError?: string;
}
export const DealTicketAmount = ({
type,
marketData,
marketPrice,
...props
}: DealTicketAmountProps) => {
switch (type) {
case Schema.OrderType.TYPE_MARKET:
return (
<DealTicketMarketAmount
{...props}
marketData={marketData}
marketPrice={marketPrice}
/>
);
case Schema.OrderType.TYPE_LIMIT:
return <DealTicketLimitAmount {...props} />;
default: {
throw new Error('Invalid ticket type ' + type);
}
}
};
@@ -1,120 +0,0 @@
import {
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
export type DealTicketLimitAmountProps = Omit<
DealTicketAmountProps,
'marketData' | 'type'
>;
export const DealTicketLimitAmount = ({
control,
market,
sizeError,
priceError,
}: DealTicketLimitAmountProps) => {
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const renderError = () => {
if (sizeError) {
return (
<TradingInputError testId="deal-ticket-error-message-size-limit">
{sizeError}
</TradingInputError>
);
}
if (priceError) {
return (
<TradingInputError testId="deal-ticket-error-message-price-limit">
{priceError}
</TradingInputError>
);
}
return null;
};
return (
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<TradingFormGroup
label={t('Size')}
labelFor="input-order-size-limit"
className="!mb-0"
>
<Controller
name="size"
control={control}
rules={{
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field, fieldState }) => (
<TradingInput
id="input-order-size-limit"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</TradingFormGroup>
</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
className="!mb-0"
>
<Controller
name="price"
control={control}
rules={{
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => (
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</TradingFormGroup>
</div>
</div>
{renderError()}
</div>
);
};
@@ -1,102 +0,0 @@
import {
addDecimalsFormatNumber,
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
TradingInput,
TradingInputError,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction } from '@vegaprotocol/markets';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { Controller } from 'react-hook-form';
import classNames from 'classnames';
export type DealTicketMarketAmountProps = Omit<DealTicketAmountProps, 'type'>;
export const DealTicketMarketAmount = ({
control,
market,
marketData,
marketPrice,
sizeError,
}: DealTicketMarketAmountProps) => {
const quoteName = market.tradableInstrument.instrument.product.quoteName;
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const price = marketPrice;
const priceFormatted = price
? addDecimalsFormatNumber(price, market.decimalPlaces)
: undefined;
const inAuction = isMarketInAuction(marketData.marketTradingMode);
return (
<div className="mb-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<div className="mb-2 text-xs">{t('Size')}</div>
<Controller
name="size"
control={control}
rules={{
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field, fieldState }) => (
<TradingInput
id="input-order-size-market"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
hasError={!!fieldState.error}
{...field}
/>
)}
/>
</div>
<div className="pt-5 leading-10">@</div>
<div className="flex-1 text-sm text-right">
{inAuction && (
<Tooltip
description={t(
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
)}
>
<div className="mb-2">{t(`Indicative price`)}</div>
</Tooltip>
)}
<div
data-testid="last-price"
className={classNames('leading-10', { 'pt-5': !inAuction })}
>
{priceFormatted && quoteName ? (
<>
~{priceFormatted} {quoteName}
</>
) : (
'-'
)}
</div>
</div>
</div>
{sizeError && (
<TradingInputError
intent="danger"
testId="deal-ticket-error-message-size-market"
>
{sizeError}
</TradingInputError>
)}
</div>
);
};
@@ -72,6 +72,7 @@ const timeInForce = 'order-tif';
const sizeErrorMessage = 'stop-order-error-message-size';
const priceErrorMessage = 'stop-order-error-message-price';
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
const triggerTrailingPercentOffsetErrorMessage =
'stop-order-error-message-trigger-trailing-percent-offset';
@@ -114,14 +115,6 @@ describe('StopOrder', () => {
});
});
it('should display trigger price as price for market type order', async () => {
render(generateJsx());
await userEvent.click(screen.getByTestId(orderTypeTrigger));
await userEvent.click(screen.getByTestId(orderTypeMarket));
await userEvent.type(screen.getByTestId(triggerPriceInput), '10');
expect(screen.getByTestId('price')).toHaveTextContent('10.0');
});
it('should use local storage state for initial values', async () => {
const values: Partial<StopOrderFormValues> = {
type: Schema.OrderType.TYPE_LIMIT,
@@ -203,8 +196,8 @@ describe('StopOrder', () => {
await userEvent.click(screen.getByTestId(submitButton));
// price error message should not show if size has error
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
// expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
// await userEvent.type(screen.getByTestId(sizeInput), '0.1');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
await userEvent.type(screen.getByTestId(priceInput), '0.001');
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
@@ -249,10 +242,17 @@ describe('StopOrder', () => {
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
// clear and fill using valid value
// clear and fill using value causing immediate trigger
await userEvent.clear(screen.getByTestId(triggerPriceInput));
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
expect(
screen.queryByTestId(triggerPriceWarningMessage)
).toBeInTheDocument();
// change to correct value
await userEvent.type(screen.getByTestId(triggerPriceInput), '2');
expect(screen.queryByTestId(triggerPriceWarningMessage)).toBeNull();
});
it('validates trigger trailing percentage offset field', async () => {
@@ -2,24 +2,26 @@ import { useRef, useCallback, useEffect } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
import {
formatNumber,
formatForInput,
removeDecimal,
toDecimal,
validateAmount,
} from '@vegaprotocol/utils';
import type { Control, UseFormWatch } from 'react-hook-form';
import { useForm, Controller, useController } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';
import {
TradingRadio,
TradingRadioGroup,
TradingInput,
TradingCheckbox,
TradingFormGroup,
TradingInputError,
TradingSelect,
TradingRadio as Radio,
TradingRadioGroup as RadioGroup,
TradingInput as Input,
TradingCheckbox as Checkbox,
TradingFormGroup as FormGroup,
TradingInputError as InputError,
TradingSelect as Select,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
@@ -34,10 +36,10 @@ import { TypeToggle } from './type-selector';
import {
useDealTicketFormValues,
DealTicketType,
type StopOrderFormValues,
dealTicketTypeToOrderType,
isStopOrderType,
} from '../../hooks/use-form-values';
import type { StopOrderFormValues } from '../../hooks/use-form-values';
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-submission';
import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
@@ -49,6 +51,8 @@ export interface StopOrderProps {
submit: (order: StopOrdersSubmission) => void;
}
const trailingPercentOffsetStep = '0.1';
const getDefaultValues = (
type: Schema.OrderType,
storedValues?: Partial<StopOrderFormValues>
@@ -62,9 +66,426 @@ const getDefaultValues = (
expire: false,
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
size: '0',
oco: false,
ocoType: type,
ocoTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
ocoTriggerType: 'price',
ocoSize: '0',
...storedValues,
});
const Trigger = ({
control,
watch,
priceStep,
assetSymbol,
oco,
marketPrice,
decimalPlaces,
}: {
control: Control<StopOrderFormValues>;
watch: UseFormWatch<StopOrderFormValues>;
priceStep: string;
assetSymbol: string;
oco?: boolean;
marketPrice?: string | null;
decimalPlaces: number;
}) => {
const triggerType = watch(oco ? 'ocoTriggerType' : 'triggerType');
const triggerDirection = watch('triggerDirection');
const isPriceTrigger = triggerType === 'price';
return (
<FormGroup label={t('Trigger')} labelFor="">
<Controller
name="triggerDirection"
control={control}
render={({ field }) => {
const { value, onChange } = field;
return (
<RadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<Radio
value={
oco
? Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
: Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id={`triggerDirection-risesAbove${oco ? '-oco' : ''}`}
label={'Rises above'}
/>
<Radio
value={
!oco
? Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
: Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id={`triggerDirection-fallsBelow${oco ? '-oco' : ''}`}
label={'Falls below'}
/>
</RadioGroup>
);
}}
/>
{isPriceTrigger && (
<div className="mb-2">
<Controller
name={oco ? 'ocoTriggerPrice' : 'triggerPrice'}
rules={{
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
control={control}
render={({ field, fieldState }) => {
const { value, ...props } = field;
let triggerWarning = false;
if (marketPrice && value) {
const condition =
(!oco &&
triggerDirection ===
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE) ||
(oco &&
triggerDirection ===
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW)
? '>'
: '<';
const diff =
BigInt(marketPrice) -
BigInt(removeDecimal(value, decimalPlaces));
if (
(condition === '>' && diff > 0) ||
(condition === '<' && diff < 0)
) {
triggerWarning = true;
}
}
return (
<>
<div className="mb-2">
<Input
data-testid={`triggerPrice${oco ? '-oco' : ''}`}
type="number"
step={priceStep}
appendElement={assetSymbol}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-trigger-price${
oco ? '-oco' : ''
}`}
>
{fieldState.error.message}
</InputError>
)}
{!fieldState.error && triggerWarning && (
<InputError
intent="warning"
testId={`stop-order-warning-message-trigger-price${
oco ? '-oco' : ''
}`}
>
{t('Stop order will be triggered immediately')}
</InputError>
)}
</>
);
}}
/>
</div>
)}
{!isPriceTrigger && (
<div className="mb-2">
<Controller
name={
oco
? 'ocoTriggerTrailingPercentOffset'
: 'triggerTrailingPercentOffset'
}
control={control}
rules={{
required: t('You need provide a trailing percent offset'),
min: {
value: trailingPercentOffsetStep,
message: t(
'Trailing percent offset cannot be lower than ' +
trailingPercentOffsetStep
),
},
max: {
value: '99.9',
message: t(
'Trailing percent offset cannot be higher than 99.9'
),
},
validate: validateAmount(
trailingPercentOffsetStep,
'Trailing percentage offset'
),
}}
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<>
<div className="mb-2">
<Input
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid={`triggerTrailingPercentOffset${
oco ? '-oco' : ''
}`}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-trigger-trailing-percent-offset${
oco ? '-oco' : ''
}`}
>
{fieldState.error.message}
</InputError>
)}
</>
);
}}
/>
</div>
)}
<Controller
name={oco ? 'ocoTriggerType' : 'triggerType'}
control={control}
rules={{
deps: oco
? ['ocoTriggerTrailingPercentOffset', 'ocoTriggerPrice']
: ['triggerTrailingPercentOffset', 'triggerPrice'],
}}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio
value="price"
id={`triggerType-price${oco ? '-oco' : ''}`}
label={'Price'}
/>
<Radio
value="trailingPercentOffset"
id={`triggerType-trailingPercentOffset${oco ? '-oco' : ''}`}
label={'Trailing Percent Offset'}
/>
</RadioGroup>
);
}}
/>
</FormGroup>
);
};
const Size = ({
control,
sizeStep,
oco,
}: {
control: Control<StopOrderFormValues>;
sizeStep: string;
oco?: boolean;
}) => {
return (
<Controller
name={oco ? 'ocoSize' : 'size'}
control={control}
rules={{
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field, fieldState }) => {
const { value, ...props } = field;
const id = `order-size${oco ? '-oco' : ''}`;
return (
<div className="mb-4">
<FormGroup labelFor={id} label={t(`Size`)} compact>
<Input
id={id}
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid={id}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</FormGroup>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-size${oco ? '-oco' : ''}`}
>
{fieldState.error.message}
</InputError>
)}
</div>
);
}}
/>
);
};
const Price = ({
control,
watch,
priceStep,
quoteName,
oco,
}: {
control: Control<StopOrderFormValues>;
watch: UseFormWatch<StopOrderFormValues>;
priceStep: string;
quoteName: string;
oco?: boolean;
}) => {
if (watch(oco ? 'ocoType' : 'type') === Schema.OrderType.TYPE_MARKET) {
return null;
}
return (
<Controller
name={oco ? 'ocoPrice' : 'price'}
control={control}
rules={{
deps: 'type',
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => {
const { value, ...props } = field;
const id = `order-price${oco ? '-oco' : ''}`;
return (
<div className="mb-4">
<FormGroup
labelFor={id}
label={t(`Price (${quoteName})`)}
compact={true}
>
<Input
id={id}
className="w-full"
type="number"
step={priceStep}
data-testid={id}
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</FormGroup>
{fieldState.error && (
<InputError
testId={`stop-order-error-message-price${oco ? '-oco' : ''}`}
>
{fieldState.error.message}
</InputError>
)}
</div>
);
}}
/>
);
};
const TimeInForce = ({
control,
oco,
}: {
control: Control<StopOrderFormValues>;
oco?: boolean;
}) => (
<Controller
name="timeInForce"
control={control}
render={({ field, fieldState }) => {
const id = `select-time-in-force${oco ? '-oco' : ''}`;
return (
<div className="mb-2">
<FormGroup label={t('Time in force')} labelFor={id} compact={true}>
<Select
id={id}
className="w-full"
data-testid="order-tif"
hasError={!!fieldState.error}
{...field}
>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
</option>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
</option>
</Select>
</FormGroup>
{fieldState.error && (
<InputError testId={`stop-error-message-tif${oco ? '-oco' : ''}`}>
{fieldState.error.message}
</InputError>
)}
</div>
);
}}
/>
);
const ReduceOnly = () => (
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<>{t('Reduce only')}</>
</Tooltip>
}
/>
);
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const { pubKey, isReadOnly } = useVegaWallet();
const setType = useDealTicketFormValues((state) => state.setType);
@@ -107,6 +528,8 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const timeInForce = watch('timeInForce');
const rawPrice = watch('price');
const rawSize = watch('size');
const oco = watch('oco');
const expiresAt = watch('expiresAt');
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
@@ -155,12 +578,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const priceStep = toDecimal(market?.decimalPlaces);
const trailingPercentOffsetStep = '0.1';
const priceFormatted =
isPriceTrigger && triggerPrice
? formatNumber(triggerPrice, market.decimalPlaces)
: undefined;
useController({
name: 'type',
@@ -187,9 +604,9 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}}
/>
{errors.type && (
<TradingInputError testId="stop-order-error-message-type">
<InputError testId="stop-order-error-message-type">
{errors.type.message}
</TradingInputError>
</InputError>
)}
<Controller
@@ -199,303 +616,128 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<TradingFormGroup label={t('Trigger')} compact={true} labelFor="">
<Controller
name="triggerDirection"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<TradingRadioGroup
name="triggerDirection"
onChange={onChange}
value={value}
orientation="horizontal"
className="mb-2"
>
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_RISES_ABOVE
}
id="triggerDirection-risesAbove"
label={'Rises above'}
/>
<TradingRadio
value={
Schema.StopOrderTriggerDirection
.TRIGGER_DIRECTION_FALLS_BELOW
}
id="triggerDirection-fallsBelow"
label={'Falls below'}
/>
</TradingRadioGroup>
);
}}
/>
{isPriceTrigger && (
<div className="mb-2">
<Controller
name="triggerPrice"
rules={{
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
control={control}
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<TradingInput
data-testid="triggerPrice"
type="number"
step={priceStep}
appendElement={asset.symbol}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
);
}}
/>
{errors.triggerPrice && (
<TradingInputError testId="stop-order-error-message-trigger-price">
{errors.triggerPrice.message}
</TradingInputError>
)}
</div>
)}
{!isPriceTrigger && (
<div className="mb-2">
<Controller
name="triggerTrailingPercentOffset"
control={control}
rules={{
required: t('You need provide a trailing percent offset'),
min: {
value: trailingPercentOffsetStep,
message: t(
'Trailing percent offset cannot be lower than ' +
trailingPercentOffsetStep
),
},
max: {
value: '99.9',
message: t(
'Trailing percent offset cannot be higher than 99.9'
),
},
validate: validateAmount(
trailingPercentOffsetStep,
'Trailing percentage offset'
),
}}
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<div className="mb-2">
<TradingInput
type="number"
step={trailingPercentOffsetStep}
appendElement="%"
data-testid="triggerTrailingPercentOffset"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
</div>
);
}}
/>
{errors.triggerTrailingPercentOffset && (
<TradingInputError testId="stop-order-error-message-trigger-trailing-percent-offset">
{errors.triggerTrailingPercentOffset.message}
</TradingInputError>
)}
</div>
)}
<Controller
name="triggerType"
control={control}
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
render={({ field }) => {
const { onChange, value } = field;
return (
<TradingRadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<TradingRadio
value="price"
id="triggerType-price"
label={'Price'}
/>
<TradingRadio
value="trailingPercentOffset"
id="triggerType-trailingPercentOffset"
label={'Trailing Percent Offset'}
/>
</TradingRadioGroup>
);
}}
/>
</TradingFormGroup>
<div className="mb-2">
<div className="flex items-start gap-4">
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Size`)}
className="!mb-0 flex-1"
>
<Controller
name="size"
control={control}
rules={{
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<TradingInput
id="order-size"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</TradingFormGroup>
<div className="pt-5 leading-10">@</div>
<div className="flex-1">
{type === Schema.OrderType.TYPE_LIMIT ? (
<TradingFormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
labelAlign="right"
className="!mb-0"
>
<Controller
name="price"
control={control}
rules={{
deps: 'type',
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => {
const { value, ...props } = field;
return (
<TradingInput
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
value={value || ''}
hasError={!!fieldState.error}
{...props}
/>
);
}}
/>
</TradingFormGroup>
) : (
<div
className="text-sm text-right pt-5 leading-10"
data-testid="price"
>
{priceFormatted && quoteName
? `~${priceFormatted} ${quoteName}`
: '-'}
</div>
)}
</div>
</div>
{errors.size && (
<TradingInputError testId="stop-order-error-message-size">
{errors.size.message}
</TradingInputError>
)}
{!errors.size &&
errors.price &&
type === Schema.OrderType.TYPE_LIMIT && (
<TradingInputError testId="stop-order-error-message-price">
{errors.price.message}
</TradingInputError>
)}
<Trigger
control={control}
watch={watch}
priceStep={priceStep}
assetSymbol={asset.symbol}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
/>
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
<Price
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
/>
<Size control={control} sizeStep={sizeStep} />
<TimeInForce control={control} />
<div className="flex justify-end pb-3 gap-2">
<ReduceOnly />
</div>
<div className="mb-2">
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Controller
name="timeInForce"
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
<div className="flex justify-between pb-2 gap-2">
<Controller
name="oco"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<Checkbox
onCheckedChange={(state) => {
onChange(state);
setValue(
'expiryStrategy',
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
);
}}
checked={value}
name="oco"
label={
<Tooltip
description={<span>{t('One cancels another')}</span>}
>
<>{t('OCO')}</>
</Tooltip>
}
/>
);
}}
/>
</div>
{oco && (
<>
<FormGroup label={t('Type')} labelFor="">
<Controller
name={`ocoType`}
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio
value={Schema.OrderType.TYPE_MARKET}
id={`ocoTypeMarket`}
label={'Market'}
/>
<Radio
value={Schema.OrderType.TYPE_LIMIT}
id={`ocoTypeLimit`}
label={'Limit'}
/>
</RadioGroup>
);
}}
/>
</FormGroup>
<Trigger
control={control}
render={({ field, fieldState }) => (
<TradingSelect
id="select-time-in-force"
className="w-full"
data-testid="order-tif"
hasError={!!fieldState.error}
{...field}
>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
</option>
<option
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
>
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
</option>
</TradingSelect>
)}
watch={watch}
priceStep={priceStep}
assetSymbol={asset.symbol}
marketPrice={marketPrice}
decimalPlaces={market.decimalPlaces}
oco
/>
</TradingFormGroup>
{errors.timeInForce && (
<TradingInputError testId="stop-error-message-tif">
{errors.timeInForce.message}
</TradingInputError>
)}
</div>
<div className="flex gap-2 pb-2 justify-between">
<hr className="mb-2 border-vega-clight-500 dark:border-vega-cdark-500" />
<Price
control={control}
watch={watch}
priceStep={priceStep}
quoteName={quoteName}
oco
/>
<Size control={control} sizeStep={sizeStep} oco />
<TimeInForce control={control} oco />
<div className="flex justify-end mb-2 gap-2">
<ReduceOnly />
</div>
</>
)}
<div className="mb-2">
<Controller
name="expire"
control={control}
render={({ field }) => {
const { onChange: onCheckedChange, value } = field;
return (
<TradingCheckbox
onCheckedChange={onCheckedChange}
<Checkbox
onCheckedChange={(value) => {
if (
value &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
) {
setValue('expiresAt', formatForInput(new Date()), {
shouldValidate: true,
});
}
onCheckedChange(value);
}}
checked={value}
name="expire"
label={t('Expire')}
@@ -503,54 +745,47 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
);
}}
/>
<TradingCheckbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<>{t('Reduce only')}</>
</Tooltip>
}
/>
</div>
{expire && (
<>
<TradingFormGroup
label={t('Strategy')}
labelFor="expiryStrategy"
compact={true}
>
<FormGroup label={t('Strategy')} labelFor="expiryStrategy">
<Controller
name="expiryStrategy"
control={control}
render={({ field }) => {
const { onChange, value } = field;
return (
<TradingRadioGroup orientation="horizontal" {...field}>
<TradingRadio
<RadioGroup
onChange={onChange}
value={value}
orientation="horizontal"
>
<Radio
disabled={oco}
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
}
id="expiryStrategy-submit"
label={'Submit'}
/>
<TradingRadio
<Radio
value={
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
}
id="expiryStrategy-cancel"
label={'Cancel'}
/>
</TradingRadioGroup>
</RadioGroup>
);
}}
/>
</TradingFormGroup>
<div className="mb-2">
</FormGroup>
<div className="mb-4">
<Controller
name="expiresAt"
control={control}
rules={{
required: t('You need provide a expiry time/date'),
validate: validateExpiration,
}}
render={({ field }) => {
@@ -7,7 +7,6 @@ import { DealTicket } from './deal-ticket';
import * as Schema from '@vegaprotocol/types';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { addDecimal } from '@vegaprotocol/utils';
import type { OrdersQuery } from '@vegaprotocol/orders';
import {
DealTicketType,
@@ -135,20 +134,6 @@ describe('DealTicket', () => {
);
});
it('should display last price for market type order', () => {
render(generateJsx());
act(() => {
screen.getByTestId('order-type-Market').click();
});
// Assert last price is shown
expect(screen.getByTestId('last-price')).toHaveTextContent(
// eslint-disable-next-line
`~${addDecimal(marketPrice, market.decimalPlaces)} ${
market.tradableInstrument.instrument.product.quoteName
}`
);
});
it('should use local storage state for initial values', () => {
const expectedOrder = {
marketId: market.id,
@@ -3,7 +3,6 @@ import * as Schema from '@vegaprotocol/types';
import type { FormEventHandler } from 'react';
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
import { Controller, useController, useForm } from 'react-hook-form';
import { DealTicketAmount } from './deal-ticket-amount';
import { DealTicketButton } from './deal-ticket-button';
import {
DealTicketFeeDetails,
@@ -17,8 +16,10 @@ import type { OrderSubmission } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from '../../utils/map-form-values-to-submission';
import {
TradingCheckbox,
TradingInputError,
TradingInput as Input,
TradingCheckbox as Checkbox,
TradingFormGroup as FormGroup,
TradingInputError as InputError,
Intent,
Notification,
Tooltip,
@@ -28,11 +29,15 @@ import {
useEstimatePositionQuery,
useOpenVolume,
} from '@vegaprotocol/positions';
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
import {
toBigNum,
removeDecimal,
validateAmount,
toDecimal,
formatForInput,
} from '@vegaprotocol/utils';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { OrderInfo } from '@vegaprotocol/types';
import {
validateExpiration,
validateMarketState,
@@ -52,8 +57,6 @@ import {
useMarketAccountBalance,
useAccountBalance,
} from '@vegaprotocol/accounts';
import { OrderType } from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/data-provider';
import {
DealTicketType,
@@ -64,6 +67,7 @@ import type { OrderFormValues } from '../../hooks/use-form-values';
import { useDealTicketFormValues } from '../../hooks/use-form-values';
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop';
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
export const REDUCE_ONLY_TOOLTIP =
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
@@ -168,6 +172,7 @@ export const DealTicket = ({
const rawPrice = watch('price');
const iceberg = watch('iceberg');
const peakSize = watch('peakSize');
const expiresAt = watch('expiresAt');
useEffect(() => {
const size = storedFormValues?.[dealTicketType]?.size;
@@ -222,8 +227,8 @@ export const DealTicket = ({
});
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
const orders = activeOrders
? activeOrders.map<OrderInfo>((order) => ({
isMarketOrder: order.type === OrderType.TYPE_MARKET,
? activeOrders.map<Schema.OrderInfo>((order) => ({
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
price: order.price,
remaining: order.remaining,
side: order.side,
@@ -231,7 +236,7 @@ export const DealTicket = ({
: [];
if (normalizedOrder) {
orders.push({
isMarketOrder: normalizedOrder.type === OrderType.TYPE_MARKET,
isMarketOrder: normalizedOrder.type === Schema.OrderType.TYPE_MARKET,
price: normalizedOrder.price ?? '0',
remaining: normalizedOrder.size,
side: normalizedOrder.side,
@@ -299,12 +304,10 @@ export const DealTicket = ({
pubKey,
]);
const disablePostOnlyCheckbox = [
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
].includes(timeInForce);
const disableReduceOnlyCheckbox = !disablePostOnlyCheckbox;
const nonPersistentOrder = isNonPersistentOrder(timeInForce);
const disablePostOnlyCheckbox = nonPersistentOrder;
const disableReduceOnlyCheckbox = !nonPersistentOrder;
const disableIcebergCheckbox = nonPersistentOrder;
const onSubmit = useCallback(
(formValues: OrderFormValues) => {
@@ -332,6 +335,10 @@ export const DealTicket = ({
},
});
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return (
<form
onSubmit={
@@ -366,15 +373,82 @@ export const DealTicket = ({
<SideSelector value={field.value} onValueChange={field.onChange} />
)}
/>
<DealTicketAmount
type={type}
<Controller
name="size"
control={control}
market={market}
marketData={marketData}
marketPrice={marketPrice || undefined}
sizeError={errors.size?.message}
priceError={errors.price?.message}
rules={{
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
}}
render={({ field, fieldState }) => (
<div className="mb-4">
<FormGroup
label={t('Size')}
labelFor="input-order-size-limit"
compact
>
<Input
id="input-order-size-limit"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
</FormGroup>
{fieldState.error && (
<InputError testId="deal-ticket-error-message-size">
{fieldState.error.message}
</InputError>
)}
</div>
)}
/>
{type === Schema.OrderType.TYPE_LIMIT && (
<Controller
name="price"
control={control}
rules={{
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
validate: validateAmount(priceStep, 'Price'),
}}
render={({ field, fieldState }) => (
<div className="mb-4">
<FormGroup
labelFor="input-price-quote"
label={t(`Price (${quoteName})`)}
compact
>
<Input
id="input-price-quote"
className="w-full"
type="number"
step={priceStep}
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...field}
/>
</FormGroup>
{fieldState.error && (
<InputError testId="deal-ticket-error-message-price">
{fieldState.error.message}
</InputError>
)}
</div>
)}
/>
)}
<Controller
name="timeInForce"
control={control}
@@ -388,7 +462,25 @@ export const DealTicket = ({
<TimeInForceSelector
value={field.value}
orderType={type}
onSelect={field.onChange}
onSelect={(value) => {
// If GTT is selected and no expiresAt time is set, or its
// behind current time then reset the value to current time
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
) {
setValue('expiresAt', formatForInput(new Date()), {
shouldValidate: true,
});
}
// iceberg orders must be persistent orders, so if user
// switches to to a non persisten tif value, remove iceberg selection
if (iceberg && isNonPersistentOrder(value)) {
setValue('iceberg', false);
}
field.onChange(value);
}}
market={market}
marketData={marketData}
errorMessage={errors.timeInForce?.message}
@@ -401,6 +493,7 @@ export const DealTicket = ({
name="expiresAt"
control={control}
rules={{
required: t('You need provide a expiry time/date'),
validate: validateExpiration,
}}
render={({ field }) => (
@@ -412,12 +505,12 @@ export const DealTicket = ({
)}
/>
)}
<div className="flex gap-2 pb-2 justify-between">
<div className="flex justify-between pb-2 gap-2">
<Controller
name="postOnly"
control={control}
render={({ field }) => (
<TradingCheckbox
<Checkbox
name="post-only"
checked={!disablePostOnlyCheckbox && field.value}
disabled={disablePostOnlyCheckbox}
@@ -449,7 +542,7 @@ export const DealTicket = ({
name="reduceOnly"
control={control}
render={({ field }) => (
<TradingCheckbox
<Checkbox
name="reduce-only"
checked={!disableReduceOnlyCheckbox && field.value}
disabled={disableReduceOnlyCheckbox}
@@ -478,15 +571,16 @@ export const DealTicket = ({
</div>
{type === Schema.OrderType.TYPE_LIMIT && (
<>
<div className="flex gap-2 pb-2 justify-between">
<div className="flex justify-between pb-2 gap-2">
<Controller
name="iceberg"
control={control}
render={({ field }) => (
<TradingCheckbox
<Checkbox
name="iceberg"
checked={field.value}
onCheckedChange={field.onChange}
disabled={disableIcebergCheckbox}
label={
<Tooltip
description={
@@ -572,11 +666,11 @@ export const NoWalletWarning = ({
if (isReadOnly) {
return (
<div className="mb-2">
<TradingInputError testId="deal-ticket-error-message-summary">
<InputError testId="deal-ticket-error-message-summary">
{
'You need to connect your own wallet to start trading on this market'
}
</TradingInputError>
</InputError>
</div>
);
}
@@ -613,9 +707,9 @@ const SummaryMessage = memo(
if (error?.message) {
return (
<div className="mb-2">
<TradingInputError testId="deal-ticket-error-message-summary">
<InputError testId="deal-ticket-error-message-summary">
{error?.message}
</TradingInputError>
</InputError>
</div>
);
}
@@ -18,30 +18,30 @@ export const ExpirySelector = ({
onSelect,
errorMessage,
}: ExpirySelectorProps) => {
const now = useRef(new Date());
const date = value ? new Date(value) : now.current;
const dateFormatted = formatForInput(date);
const minDate = formatForInput(date);
const minDateRef = useRef(new Date());
return (
<TradingFormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact={true}
>
<TradingInput
data-testid="date-picker-field"
id="expiration"
type="datetime-local"
value={dateFormatted}
onChange={(e) => onSelect(e.target.value)}
min={minDate}
hasError={!!errorMessage}
/>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</TradingInputError>
)}
</TradingFormGroup>
<div className="mb-4">
<TradingFormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact
>
<TradingInput
data-testid="date-picker-field"
id="expiration"
type="datetime-local"
value={value && formatForInput(new Date(value))}
onChange={(e) => onSelect(e.target.value)}
min={formatForInput(minDateRef.current)}
hasError={!!errorMessage}
/>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-expiry">
{errorMessage}
</TradingInputError>
)}
</TradingFormGroup>
</div>
);
};
@@ -1,7 +1,4 @@
export * from './deal-ticket-amount';
export * from './deal-ticket-container';
export * from './deal-ticket-limit-amount';
export * from './deal-ticket-market-amount';
export * from './deal-ticket';
export * from './deal-ticket-stop-order';
export * from './deal-ticket-container';
@@ -90,32 +90,34 @@ export const TimeInForceSelector = ({
};
return (
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<TradingSelect
id="select-time-in-force"
value={value}
onChange={(e) => {
onSelect(e.target.value as Schema.OrderTimeInForce);
}}
className="w-full"
data-testid="order-tif"
hasError={!!errorMessage}
<div className="mb-4">
<TradingFormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
{options.map(([key, value]) => (
<option key={key} value={value}>
{timeInForceLabel(value)}
</option>
))}
</TradingSelect>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</TradingInputError>
)}
</TradingFormGroup>
<TradingSelect
id="select-time-in-force"
value={value}
onChange={(e) => {
onSelect(e.target.value as Schema.OrderTimeInForce);
}}
className="w-full"
data-testid="order-tif"
hasError={!!errorMessage}
>
{options.map(([key, value]) => (
<option key={key} value={value}>
{timeInForceLabel(value)}
</option>
))}
</TradingSelect>
{errorMessage && (
<TradingInputError testId="deal-ticket-error-message-tif">
{renderError(errorMessage)}
</TradingInputError>
)}
</TradingFormGroup>
</div>
);
};
@@ -76,7 +76,7 @@ export const TypeToggle = ({
<TradingDropdownTrigger
data-testid="order-type-Stop"
className={classNames(
'rounded px-3 flex flex-nowrap items-center justify-center',
'rounded px-2 flex flex-nowrap items-center justify-center',
{
'bg-vega-clight-500 dark:bg-vega-cdark-500': selectedOption,
}
@@ -28,6 +28,17 @@ export interface StopOrderFormValues {
expire: boolean;
expiryStrategy?: Schema.StopOrderExpiryStrategy;
expiresAt?: string;
oco?: boolean;
ocoTriggerType: 'price' | 'trailingPercentOffset';
ocoTriggerPrice?: string;
ocoTriggerTrailingPercentOffset?: string;
ocoType: OrderType;
ocoSize: string;
ocoTimeInForce: OrderTimeInForce;
ocoPrice?: string;
}
export type OrderFormValues = {
@@ -138,6 +149,7 @@ export const useDealTicketFormValues = create<Store>()(
})),
{
name: 'vega_deal_ticket_store',
version: 1,
}
)
)
@@ -9,6 +9,7 @@ import type {
} from '../hooks/use-form-values';
import * as Schema from '@vegaprotocol/types';
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
import { isPersistentOrder } from './time-in-force-persistance';
export const mapFormValuesToOrderSubmission = (
order: OrderFormValues,
@@ -41,11 +42,8 @@ export const mapFormValuesToOrderSubmission = (
? false
: order.reduceOnly,
icebergOpts:
(order.type === Schema.OrderType.TYPE_MARKET ||
[
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(order.timeInForce)) &&
order.type === Schema.OrderType.TYPE_LIMIT &&
isPersistentOrder(order.timeInForce) &&
order.iceberg &&
order.peakSize &&
order.minimumVisibleSize
@@ -59,6 +57,22 @@ export const mapFormValuesToOrderSubmission = (
: undefined,
});
const setTrigger = (
stopOrderSetup: StopOrderSetup,
triggerType: StopOrderFormValues['triggerPrice'],
triggerPrice: StopOrderFormValues['triggerPrice'],
triggerTrailingPercentOffset: StopOrderFormValues['triggerTrailingPercentOffset'],
decimalPlaces: number
) => {
if (triggerType === 'price') {
stopOrderSetup.price = removeDecimal(triggerPrice ?? '', decimalPlaces);
} else if (triggerType === 'trailingPercentOffset') {
stopOrderSetup.trailingPercentOffset = (
Number(triggerTrailingPercentOffset) / 100
).toFixed(3);
}
};
export const mapFormValuesToStopOrdersSubmission = (
data: StopOrderFormValues,
marketId: string,
@@ -81,31 +95,46 @@ export const mapFormValuesToStopOrdersSubmission = (
positionDecimalPlaces
),
};
if (data.triggerType === 'price') {
stopOrderSetup.price = removeDecimal(
data.triggerPrice ?? '',
setTrigger(
stopOrderSetup,
data.triggerType,
data.triggerPrice,
data.triggerTrailingPercentOffset,
decimalPlaces
);
let oppositeStopOrderSetup: StopOrderSetup | undefined = undefined;
if (data.oco) {
oppositeStopOrderSetup = {
orderSubmission: mapFormValuesToOrderSubmission(
{
type: data.ocoType,
side: data.side,
size: data.ocoSize,
timeInForce: data.ocoTimeInForce,
price: data.ocoPrice,
reduceOnly: true,
},
marketId,
decimalPlaces,
positionDecimalPlaces
),
};
setTrigger(
oppositeStopOrderSetup,
data.ocoTriggerType,
data.ocoTriggerPrice,
data.ocoTriggerTrailingPercentOffset,
decimalPlaces
);
} else if (data.triggerType === 'trailingPercentOffset') {
stopOrderSetup.trailingPercentOffset = (
Number(data.triggerTrailingPercentOffset) / 100
).toFixed(3);
}
if (data.expire) {
stopOrderSetup.expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
if (
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
) {
stopOrderSetup.expiryStrategy =
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS;
} else if (
data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
) {
stopOrderSetup.expiryStrategy =
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT;
const expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
stopOrderSetup.expiresAt = expiresAt;
stopOrderSetup.expiryStrategy = data.expiryStrategy;
if (oppositeStopOrderSetup) {
oppositeStopOrderSetup.expiresAt = expiresAt;
oppositeStopOrderSetup.expiryStrategy = data.expiryStrategy;
}
}
@@ -114,12 +143,14 @@ export const mapFormValuesToStopOrdersSubmission = (
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
) {
submission.risesAbove = stopOrderSetup;
submission.fallsBelow = oppositeStopOrderSetup;
}
if (
data.triggerDirection ===
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
) {
submission.fallsBelow = stopOrderSetup;
submission.risesAbove = oppositeStopOrderSetup;
}
return submission;
@@ -1,6 +1,8 @@
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { mapFormValuesToOrderSubmission } from './map-form-values-to-submission';
import * as Schema from '@vegaprotocol/types';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
import type { OrderFormValues } from '../hooks';
describe('mapFormValuesToOrderSubmission', () => {
it('sets and formats price only for limit orders', () => {
@@ -25,7 +27,7 @@ describe('mapFormValuesToOrderSubmission', () => {
).toEqual('10000');
});
it('sets and formats expiresAt only for time in force orders', () => {
it('sets and formats expiresAt only for GTT orders', () => {
expect(
mapFormValuesToOrderSubmission(
{
@@ -49,6 +51,41 @@ describe('mapFormValuesToOrderSubmission', () => {
).toEqual('1640995200000000000');
});
it('sets and formats icebergOpts only for persisted orders', () => {
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_LIMIT,
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
iceberg: true,
peakSize: '10.00',
minimumVisibleSize: '10.00',
} as OrderFormValues,
'marketId',
2,
2
).icebergOpts
).toEqual(undefined);
expect(
mapFormValuesToOrderSubmission(
{
type: OrderType.TYPE_LIMIT,
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
iceberg: true,
peakSize: '10.00',
minimumVisibleSize: '10.00',
} as OrderFormValues,
'marketId',
2,
2
).icebergOpts
).toEqual({
peakSize: '1000',
minimumVisibleSize: '1000',
});
});
it('formats size', () => {
expect(
mapFormValuesToOrderSubmission(
@@ -0,0 +1,23 @@
import { OrderTimeInForce } from '@vegaprotocol/types';
import {
isNonPersistentOrder,
isPersistentOrder,
} from './time-in-force-persistance';
it('isNonPeristentOrder', () => {
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTT)).toBe(false);
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFA)).toBe(false);
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
});
it('isPeristentOrder', () => {
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTT)).toBe(true);
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFA)).toBe(true);
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(true);
});
@@ -0,0 +1,12 @@
import { OrderTimeInForce } from '@vegaprotocol/types';
export const isNonPersistentOrder = (timeInForce: OrderTimeInForce) => {
return [
OrderTimeInForce.TIME_IN_FORCE_FOK,
OrderTimeInForce.TIME_IN_FORCE_IOC,
].includes(timeInForce);
};
export const isPersistentOrder = (timeInForce: OrderTimeInForce) => {
return !isNonPersistentOrder(timeInForce);
};
@@ -408,6 +408,12 @@ function compileFeatureFlags(): FeatureFlags {
process.env['NX_PRODUCT_PERPETUALS']
) as string
),
METAMASK_SNAPS: TRUTHY.includes(
windowOrDefault(
'NX_METAMASK_SNAPS',
process.env['NX_METAMASK_SNAPS']
) as string
),
};
const EXPLORER_FLAGS = {
EXPLORER_ASSETS: TRUTHY.includes(
+5 -1
View File
@@ -18,7 +18,11 @@ export type Environment = z.infer<typeof envSchema>;
export type FeatureFlags = z.infer<typeof featureFlagsSchema>;
export type CosmicElevatorFlags = Pick<
FeatureFlags,
'ICEBERG_ORDERS' | 'STOP_ORDERS' | 'SUCCESSOR_MARKETS' | 'PRODUCT_PERPETUALS'
| 'ICEBERG_ORDERS'
| 'STOP_ORDERS'
| 'SUCCESSOR_MARKETS'
| 'PRODUCT_PERPETUALS'
| 'METAMASK_SNAPS'
>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
export const CUSTOM_NODE_KEY = 'custom' as const;
@@ -77,6 +77,7 @@ const COSMIC_ELEVATOR_FLAGS = {
STOP_ORDERS: z.optional(z.boolean()),
ICEBERG_ORDERS: z.optional(z.boolean()),
PRODUCT_PERPETUALS: z.optional(z.boolean()),
METAMASK_SNAPS: z.optional(z.boolean()),
};
const EXPLORER_FLAGS = {
+1 -2
View File
@@ -1,2 +1 @@
export * from './lib/ledger-manager';
export * from './lib/__generated__/LedgerEntries';
export * from './lib/ledger-export-form';
-47
View File
@@ -1,47 +0,0 @@
fragment LedgerEntry on AggregatedLedgerEntry {
vegaTime
quantity
assetId
transferType
toAccountType
toAccountMarketId
toAccountPartyId
toAccountBalance
fromAccountType
fromAccountMarketId
fromAccountPartyId
fromAccountBalance
}
query LedgerEntries(
$partyId: ID!
$pagination: Pagination
$dateRange: DateRange
$fromAccountType: [AccountType!]
$toAccountType: [AccountType!]
) {
ledgerEntries(
filter: {
FromAccountFilter: {
partyIds: [$partyId]
accountTypes: $fromAccountType
}
ToAccountFilter: { partyIds: [$partyId], accountTypes: $toAccountType }
}
pagination: $pagination
dateRange: $dateRange
) {
edges {
node {
...LedgerEntry
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
-88
View File
@@ -1,88 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type LedgerEntryFragment = { __typename?: 'AggregatedLedgerEntry', vegaTime: any, quantity: string, assetId?: string | null, transferType?: Types.TransferType | null, toAccountType?: Types.AccountType | null, toAccountMarketId?: string | null, toAccountPartyId?: string | null, toAccountBalance: string, fromAccountType?: Types.AccountType | null, fromAccountMarketId?: string | null, fromAccountPartyId?: string | null, fromAccountBalance: string };
export type LedgerEntriesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
pagination?: Types.InputMaybe<Types.Pagination>;
dateRange?: Types.InputMaybe<Types.DateRange>;
fromAccountType?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
toAccountType?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
}>;
export type LedgerEntriesQuery = { __typename?: 'Query', ledgerEntries: { __typename?: 'AggregatedLedgerEntriesConnection', edges: Array<{ __typename?: 'AggregatedLedgerEntriesEdge', cursor: string, node: { __typename?: 'AggregatedLedgerEntry', vegaTime: any, quantity: string, assetId?: string | null, transferType?: Types.TransferType | null, toAccountType?: Types.AccountType | null, toAccountMarketId?: string | null, toAccountPartyId?: string | null, toAccountBalance: string, fromAccountType?: Types.AccountType | null, fromAccountMarketId?: string | null, fromAccountPartyId?: string | null, fromAccountBalance: string } } | null>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } };
export const LedgerEntryFragmentDoc = gql`
fragment LedgerEntry on AggregatedLedgerEntry {
vegaTime
quantity
assetId
transferType
toAccountType
toAccountMarketId
toAccountPartyId
toAccountBalance
fromAccountType
fromAccountMarketId
fromAccountPartyId
fromAccountBalance
}
`;
export const LedgerEntriesDocument = gql`
query LedgerEntries($partyId: ID!, $pagination: Pagination, $dateRange: DateRange, $fromAccountType: [AccountType!], $toAccountType: [AccountType!]) {
ledgerEntries(
filter: {FromAccountFilter: {partyIds: [$partyId], accountTypes: $fromAccountType}, ToAccountFilter: {partyIds: [$partyId], accountTypes: $toAccountType}}
pagination: $pagination
dateRange: $dateRange
) {
edges {
node {
...LedgerEntry
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
${LedgerEntryFragmentDoc}`;
/**
* __useLedgerEntriesQuery__
*
* To run a query within a React component, call `useLedgerEntriesQuery` and pass it any options that fit your needs.
* When your component renders, `useLedgerEntriesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLedgerEntriesQuery({
* variables: {
* partyId: // value for 'partyId'
* pagination: // value for 'pagination'
* dateRange: // value for 'dateRange'
* fromAccountType: // value for 'fromAccountType'
* toAccountType: // value for 'toAccountType'
* },
* });
*/
export function useLedgerEntriesQuery(baseOptions: Apollo.QueryHookOptions<LedgerEntriesQuery, LedgerEntriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<LedgerEntriesQuery, LedgerEntriesQueryVariables>(LedgerEntriesDocument, options);
}
export function useLedgerEntriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<LedgerEntriesQuery, LedgerEntriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<LedgerEntriesQuery, LedgerEntriesQueryVariables>(LedgerEntriesDocument, options);
}
export type LedgerEntriesQueryHookResult = ReturnType<typeof useLedgerEntriesQuery>;
export type LedgerEntriesLazyQueryHookResult = ReturnType<typeof useLedgerEntriesLazyQuery>;
export type LedgerEntriesQueryResult = Apollo.QueryResult<LedgerEntriesQuery, LedgerEntriesQueryVariables>;
@@ -1,77 +0,0 @@
import type { Asset } from '@vegaprotocol/assets';
import { assetsMapProvider } from '@vegaprotocol/assets';
import type { Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets';
import {
makeDataProvider,
makeDerivedDataProvider,
} from '@vegaprotocol/data-provider';
import type {
LedgerEntriesQuery,
LedgerEntriesQueryVariables,
LedgerEntryFragment,
} from './__generated__/LedgerEntries';
import { LedgerEntriesDocument } from './__generated__/LedgerEntries';
export type LedgerEntry = LedgerEntryFragment & {
asset: Asset | null | undefined;
marketSender: Market | null | undefined;
marketReceiver: Market | null | undefined;
};
type Edge = LedgerEntriesQuery['ledgerEntries']['edges'][number];
const isLedgerEntryEdge = (entry: Edge): entry is NonNullable<Edge> =>
entry !== null;
const getData = (responseData: LedgerEntriesQuery | null) => {
return (
responseData?.ledgerEntries?.edges
.filter(isLedgerEntryEdge)
.map((edge) => edge.node) || []
);
};
const ledgerEntriesOnlyProvider = makeDataProvider<
LedgerEntriesQuery,
ReturnType<typeof getData>,
never,
never,
LedgerEntriesQueryVariables
>({
query: LedgerEntriesDocument,
getData,
additionalContext: {
isEnlargedTimeout: true,
},
});
export const ledgerEntriesProvider = makeDerivedDataProvider<
LedgerEntry[],
never,
LedgerEntriesQueryVariables
>(
[
ledgerEntriesOnlyProvider,
(callback, client) => assetsMapProvider(callback, client, undefined),
(callback, client) => marketsMapProvider(callback, client, undefined),
],
(partsData) => {
const entries = partsData[0] as ReturnType<typeof getData>;
const assets = partsData[1] as Record<string, Asset>;
const markets = partsData[2] as Record<string, Market>;
return entries.map((entry) => {
const asset = entry.assetId
? (assets as Record<string, Asset>)[entry.assetId]
: null;
const marketSender = entry.fromAccountMarketId
? markets[entry.fromAccountMarketId]
: null;
const marketReceiver = entry.toAccountMarketId
? markets[entry.toAccountMarketId]
: null;
return { ...entry, asset, marketSender, marketReceiver };
});
}
);
-247
View File
@@ -1,247 +0,0 @@
import type {
LedgerEntriesQuery,
LedgerEntryFragment,
} from './__generated__/LedgerEntries';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import merge from 'lodash/merge';
export const ledgerEntriesQuery = (
override?: PartialDeep<LedgerEntriesQuery>
): LedgerEntriesQuery => {
const defaultResult: LedgerEntriesQuery = {
__typename: 'Query',
ledgerEntries: {
__typename: 'AggregatedLedgerEntriesConnection',
edges: ledgerEntries.map((node) => ({
__typename: 'AggregatedLedgerEntriesEdge',
node,
cursor: 'cursor-1',
})),
pageInfo: {
startCursor:
'eyJ2ZWdhX3RpbWUiOiIyMDIyLTExLTIzVDE3OjI3OjU2LjczNDM2NFoifQ==',
endCursor:
'eyJ2ZWdhX3RpbWUiOiIyMDIyLTExLTIzVDEzOjExOjE2LjU0NjM2M1oifQ==',
hasNextPage: false,
hasPreviousPage: false,
__typename: 'PageInfo',
},
},
};
return merge(defaultResult, override);
};
export const ledgerEntries: LedgerEntryFragment[] = [
{
vegaTime: '1669224476734364000',
quantity: '0',
assetId: 'asset-id',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: 'market-1',
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: 'market-0',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669221452175594000',
quantity: '0',
assetId: 'asset-id-2',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: 'market-0',
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: 'market-2',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209347054198000',
quantity: '0',
assetId: 'asset-id',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: 'market-3',
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: 'market-2',
fromAccountPartyId: 'sender party id',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209345512806000',
quantity: '0',
assetId: 'asset-id',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209316163397000',
quantity: '0',
assetId: 'asset-id-2',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209299051286000',
quantity: '1326783',
assetId: 'asset-id-2',
transferType: Schema.TransferType.TRANSFER_TYPE_MTM_WIN,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209151328614000',
quantity: '0',
assetId: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209117655380000',
quantity: '0',
assetId: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209082788024000',
quantity: '1326783',
assetId: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
transferType: Schema.TransferType.TRANSFER_TYPE_MTM_WIN,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '1669209076546363000',
quantity: '0',
assetId: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_HIGH,
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T13:36:42.13989Z',
quantity: '9078407730948615',
assetId: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_LOW,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
toAccountMarketId: null,
toAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_MARGIN,
fromAccountMarketId:
'0942d767cb2cb5a795e14216e8e53c2b6f75e46dc1732c5aeda8a5aba4ad193d',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T13:35:49.257039Z',
quantity: '263142253070974',
assetId: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
transferType: Schema.TransferType.TRANSFER_TYPE_MARGIN_LOW,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
toAccountMarketId: null,
toAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_MARGIN,
fromAccountMarketId:
'0942d767cb2cb5a795e14216e8e53c2b6f75e46dc1732c5aeda8a5aba4ad193d',
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:41:22.054428Z',
quantity: '1000000000',
assetId: '4e4e80abff30cab933b8c4ac6befc618372eb76b2cbddc337eff0b4a3a4d25b8',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:39:11.516154Z',
quantity: '1000000000000',
assetId: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:37:26.832226Z',
quantity: '10000000000000000000000',
assetId: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
{
vegaTime: '2022-11-24T12:24:52.844901Z',
quantity: '49390000000000000000000',
assetId: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
transferType: Schema.TransferType.TRANSFER_TYPE_DEPOSIT,
toAccountType: Schema.AccountType.ACCOUNT_TYPE_EXTERNAL,
toAccountMarketId: null,
toAccountPartyId: 'network',
fromAccountType: Schema.AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountMarketId: null,
fromAccountPartyId:
'2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
__typename: 'AggregatedLedgerEntry',
toAccountBalance: '0',
fromAccountBalance: '0',
},
];
@@ -0,0 +1,215 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { createDownloadUrl, LedgerExportForm } from './ledger-export-form';
import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils';
const vegaUrl = 'https://vega-url.co.uk/querystuff';
const mockResponse = {
headers: { get: jest.fn() },
blob: () => '',
};
global.fetch = jest.fn().mockResolvedValue(mockResponse);
const assetsMock = {
['a'.repeat(64)]: 'symbol asset-id',
['b'.repeat(64)]: 'symbol asset-id-2',
};
describe('LedgerExportForm', () => {
const partyId = 'c'.repeat(64);
afterEach(() => {
jest.clearAllMocks();
});
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2023-08-10T10:10:10.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('should be properly rendered', async () => {
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
// userEvent does not work with faked timers
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=1691057410000000000`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('assetID should be properly change request url', async () => {
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
fireEvent.change(screen.getByTestId('select-ledger-asset'), {
target: { value: Object.keys(assetsMock)[1] },
});
expect(screen.getByText('symbol asset-id-2')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[1]
}&dateRange.startTimestamp=1691057410000000000`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('date-from should properly change request url', async () => {
const newDate = new Date(1691230210000);
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByLabelText('Date from')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Date from'), {
target: { value: formatForInput(newDate) },
});
expect(screen.getByTestId('date-from')).toHaveValue(
`${formatForInput(newDate)}.000`
);
fireEvent.click(screen.getByTestId('ledger-download-button'));
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=${toNanoSeconds(newDate)}`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
it('date-to should properly change request url', async () => {
const newDate = new Date(1691230210000);
render(
<LedgerExportForm
partyId={partyId}
vegaUrl={vegaUrl}
assets={assetsMock}
/>
);
expect(screen.getByLabelText('Date to')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Date to'), {
target: { value: formatForInput(newDate) },
});
expect(screen.getByTestId('date-to')).toHaveValue(
`${formatForInput(newDate)}.000`
);
fireEvent.click(screen.getByTestId('ledger-download-button'));
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith(
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
Object.keys(assetsMock)[0]
}&dateRange.startTimestamp=1691057410000000000&dateRange.endTimestamp=${toNanoSeconds(
newDate
)}`
);
});
await waitFor(() => {
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
});
});
});
describe('createDownloadUrl', () => {
const fromTimestamp = 1690848000000;
const toTimestamp = 1691107200000;
const args = {
protohost: 'https://vega-url.co.uk',
partyId: 'a'.repeat(64),
assetId: 'b'.repeat(64),
dateFrom: new Date(fromTimestamp).toISOString(),
};
it('formats url with without an end date', () => {
expect(createDownloadUrl(args)).toEqual(
`${args.protohost}/api/v2/ledgerentry/export?partyId=${
args.partyId
}&assetId=${args.assetId}&dateRange.startTimestamp=${toNanoSeconds(
args.dateFrom
)}`
);
});
it('formats url with with an end date', () => {
const dateTo = new Date(toTimestamp).toISOString();
expect(
createDownloadUrl({
...args,
dateTo,
})
).toEqual(
`${args.protohost}/api/v2/ledgerentry/export?partyId=${
args.partyId
}&assetId=${args.assetId}&dateRange.startTimestamp=${toNanoSeconds(
args.dateFrom
)}&dateRange.endTimestamp=${toNanoSeconds(dateTo)}`
);
});
it('should throw if invalid args are provided', () => {
// invalid url
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, protohost: 'foo' });
}).toThrow();
// invalid partyId
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, partyId: 'z'.repeat(64) });
}).toThrow();
});
});
+172
View File
@@ -0,0 +1,172 @@
import { useRef, useState } from 'react';
import { z } from 'zod';
import {
Button,
Loader,
TradingFormGroup,
TradingInput,
TradingSelect,
} from '@vegaprotocol/ui-toolkit';
import { toNanoSeconds, VEGA_ID_REGEX } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { localLoggerFactory } from '@vegaprotocol/logger';
import { formatForInput } from '@vegaprotocol/utils';
import { subDays } from 'date-fns';
const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv';
const getProtoHost = (vegaurl: string) => {
const loc = new URL(vegaurl);
return `${loc.protocol}//${loc.host}`;
};
const downloadSchema = z.object({
protohost: z.string().url().nonempty(),
partyId: z.string().regex(VEGA_ID_REGEX).nonempty(),
assetId: z.string().regex(VEGA_ID_REGEX).nonempty(),
dateFrom: z.string().nonempty(),
dateTo: z.string().optional(),
});
export const createDownloadUrl = (args: z.infer<typeof downloadSchema>) => {
// check args from form inputs
downloadSchema.parse(args);
const params = new URLSearchParams();
params.append('partyId', args.partyId);
params.append('assetId', args.assetId);
params.append('dateRange.startTimestamp', toNanoSeconds(args.dateFrom));
if (args.dateTo) {
params.append('dateRange.endTimestamp', toNanoSeconds(args.dateTo));
}
const url = new URL(args.protohost);
url.pathname = '/api/v2/ledgerentry/export';
url.search = params.toString();
return url.toString();
};
interface Props {
partyId: string;
vegaUrl: string;
assets: Record<string, string>;
}
export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const now = useRef(new Date());
const [dateFrom, setDateFrom] = useState(() => {
return formatForInput(subDays(now.current, 7));
});
const [dateTo, setDateTo] = useState('');
const maxFromDate = formatForInput(new Date(dateTo || now.current));
const maxToDate = formatForInput(now.current);
const [isDownloading, setIsDownloading] = useState(false);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const protohost = getProtoHost(vegaUrl);
const disabled = Boolean(!assetId || isDownloading);
const assetDropDown = (
<TradingSelect
id="select-ledger-asset"
value={assetId}
onChange={(e) => {
setAssetId(e.target.value);
}}
className="w-full"
data-testid="select-ledger-asset"
disabled={isDownloading}
>
{Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}>
{assets[assetKey]}
</option>
))}
</TradingSelect>
);
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
setIsDownloading(true);
const resp = await fetch(link);
const { headers } = resp;
const nameHeader = headers.get('content-disposition');
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
const blob = await resp.blob();
if (blob) {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
}
} catch (err) {
localLoggerFactory({ application: 'ledger' }).error('Download file', err);
} finally {
setIsDownloading(false);
}
};
if (!protohost || Object.keys(assets).length === 0) {
return null;
}
return (
<form onSubmit={startDownload} className="p-4 w-[350px]">
<h2 className="mb-4">{t('Export ledger entries')}</h2>
<TradingFormGroup label={t('Select asset')} labelFor="asset">
{assetDropDown}
</TradingFormGroup>
<TradingFormGroup label={t('Date from')} labelFor="date-from">
<TradingInput
type="datetime-local"
data-testid="date-from"
id="date-from"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
disabled={disabled}
max={maxFromDate}
/>
</TradingFormGroup>
<TradingFormGroup label={t('Date to')} labelFor="date-to">
<TradingInput
type="datetime-local"
data-testid="date-to"
id="date-to"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
disabled={disabled}
max={maxToDate}
/>
</TradingFormGroup>
<div className="relative text-sm" title={t('Download all to .csv file')}>
{isDownloading && (
<div
className="absolute flex items-center justify-center w-full h-full"
data-testid="download-spinner"
>
<Loader size="small" />
</div>
)}
<Button
variant="primary"
fill
disabled={disabled}
type="submit"
data-testid="ledger-download-button"
>
{t('Download')}
</Button>
</div>
</form>
);
};
@@ -1,85 +0,0 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LedgerExportLink } from './ledger-export-link';
import * as Types from '@vegaprotocol/types';
import { ledgerEntries } from './ledger-entries.mock';
import type { LedgerEntry } from './ledger-entries-data-provider';
const VEGA_URL = 'https://vega-url.co.uk/querystuff';
const mockEnvironment = jest.fn(() => VEGA_URL);
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: jest.fn(() => mockEnvironment()),
}));
const asset = {
id: 'assetID',
name: 'assetName',
symbol: 'assetSymbol',
decimals: 1,
quantum: '1',
status: Types.AssetStatus,
source: {
__typename: 'ERC20',
contractAddress: 'contractAddres',
lifetimeLimit: 'lifetimeLimit',
withdrawThreshold: 'withdraw',
},
};
describe('LedgerExportLink', () => {
const partyId = 'partyId';
const entries = ledgerEntries.map((entry) => {
return {
...entry,
asset: entry.assetId
? {
...asset,
id: entry.assetId,
name: `name ${entry.assetId}`,
symbol: `symbol ${entry.assetId}`,
}
: null,
marketSender: null,
marketReceiver: null,
} as LedgerEntry;
});
it('should be properly rendered', async () => {
render(<LedgerExportLink partyId={partyId} entries={entries} />);
await waitFor(() => {
expect(screen.getByRole('link')).toBeInTheDocument();
expect(screen.getByRole('link')).toHaveAttribute(
'href',
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=asset-id`
);
expect(
screen.getByRole('button', { name: /^symbol asset-id/ })
).toBeInTheDocument();
});
});
it('should be properly change link url', async () => {
render(<LedgerExportLink partyId={partyId} entries={entries} />);
await waitFor(() => {
expect(
screen.getByRole('button', { name: /^symbol asset-id/ })
).toBeInTheDocument();
});
act(() => {
userEvent.click(screen.getByRole('button', { name: /^symbol asset-id/ }));
});
await waitFor(() => {
expect(screen.getByRole('menu')).toBeInTheDocument();
});
act(() => {
userEvent.click(
screen.getByRole('menuitem', { name: /^symbol asset-id-2/ })
);
});
await waitFor(() => {
expect(screen.getByRole('link')).toHaveAttribute(
'href',
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=asset-id-2`
);
});
});
});
@@ -1,71 +0,0 @@
import type { LedgerEntry } from './ledger-entries-data-provider';
import { useMemo, useState } from 'react';
import { useEnvironment } from '@vegaprotocol/environment';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Link,
Button,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
const getProtoHost = (vegaurl: string) => {
const loc = new URL(vegaurl);
return `${loc.protocol}//${loc.host}`;
};
export const LedgerExportLink = ({
partyId,
entries,
}: {
partyId: string;
entries: LedgerEntry[];
}) => {
const assets = entries.reduce((aggr, item) => {
if (item.asset && !(item.asset.id in aggr)) {
aggr[item.asset.id] = item.asset.symbol;
}
return aggr;
}, {} as Record<string, string>);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const VEGA_URL = useEnvironment((store) => store.VEGA_URL);
const protohost = VEGA_URL ? getProtoHost(VEGA_URL) : '';
const assetDropDown = useMemo(() => {
return (
<DropdownMenu
trigger={<DropdownMenuTrigger>{assets[assetId]}</DropdownMenuTrigger>}
>
<DropdownMenuContent>
{Object.keys(assets).map((assetKey) => (
<DropdownMenuItem
key={assetKey}
onSelect={() => setAssetId(assetKey)}
>
{assets[assetKey]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [assetId, assets]);
if (!protohost || !entries || entries.length === 0) {
return null;
}
return (
<div className="flex shrink items-stretch gap-2 p-2">
<div className="flex items-center">Export all</div>
{assetDropDown}
<Link
className="text-sm"
title={t('Download all to .csv file')}
href={`${protohost}/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${assetId}`}
>
<Button size="sm">{t('Download')}</Button>
</Link>
</div>
);
};
-73
View File
@@ -1,73 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community';
import { useCallback, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns';
import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
import { LedgerTable } from './ledger-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types';
import { LedgerExportLink } from './ledger-export-link';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
export interface Filter {
vegaTime?: {
value: Schema.DateRange;
};
fromAccountType?: { value: Types.AccountType[] };
toAccountType?: { value: Types.AccountType[] };
}
const defaultFilter = {
vegaTime: {
value: { start: formatRFC3339(subDays(Date.now(), 7)) },
},
};
export const LedgerManager = ({
partyId,
gridProps,
}: {
partyId: string;
gridProps: ReturnType<typeof useDataGridEvents>;
}) => {
const [filter, setFilter] = useState<Filter>(defaultFilter);
const variables = useMemo<LedgerEntriesQueryVariables>(
() => ({
partyId,
dateRange: filter?.vegaTime?.value,
pagination: {
first: 10,
},
}),
[partyId, filter?.vegaTime?.value]
);
const { data, error } = useDataProvider({
dataProvider: ledgerEntriesProvider,
variables,
skip: !variables.partyId,
});
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const updatedFilter = { ...defaultFilter, ...event.api.getFilterModel() };
setFilter(updatedFilter);
gridProps.onFilterChanged(event);
},
[gridProps]
);
return (
<div className="h-full relative">
<LedgerTable
rowData={data}
overlayNoRowsTemplate={error ? error.message : t('No entries')}
{...gridProps}
onFilterChanged={onFilterChanged}
/>
{data && <LedgerExportLink entries={data} partyId={partyId} />}
</div>
);
};
-215
View File
@@ -1,215 +0,0 @@
import {
addDecimalsFormatNumber,
fromNanoSeconds,
getDateTimeFormat,
truncateByChars,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type {
VegaValueFormatterParams,
TypedDataAgGrid,
} from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
DateRangeFilter,
MarketNameCell,
SetFilter,
} from '@vegaprotocol/datagrid';
import type * as Types from '@vegaprotocol/types';
import type { ColDef } from 'ag-grid-community';
import type { Market } from '@vegaprotocol/markets';
import {
AccountTypeMapping,
DescriptionTransferTypeMapping,
TransferTypeMapping,
} from '@vegaprotocol/types';
import type { LedgerEntry } from './ledger-entries-data-provider';
import { useMemo } from 'react';
import { formatRFC3339, subDays } from 'date-fns';
export const TransferTooltipCellComponent = ({
value,
}: {
value: Types.TransferType;
}) => {
return (
<p className="max-w-sm px-4 py-2 z-20 rounded text-sm break-word">
{value ? DescriptionTransferTypeMapping[value] : ''}
</p>
);
};
const defaultValue = { start: formatRFC3339(subDays(Date.now(), 7)) };
const dateRangeFilterParams = {
maxNextDays: 0,
defaultValue,
};
const defaultColDef = {
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
},
};
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
export const LedgerTable = (props: LedgerEntryProps) => {
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: t('Sender'),
field: 'fromAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'fromAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketSender.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) =>
value && (
<MarketNameCell value={value} data={data?.marketSender as Market} />
),
},
{
headerName: t('Receiver'),
field: 'toAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
},
field: 'toAccountType',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) =>
value ? AccountTypeMapping[value] : '-',
},
{
headerName: t('Market'),
field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
data,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) =>
value && (
<MarketNameCell
value={value}
data={data?.marketReceiver as Market}
/>
),
},
{
headerName: t('Transfer type'),
field: 'transferType',
tooltipField: 'transferType',
filter: SetFilter,
filterParams: {
set: TransferTypeMapping,
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'transferType'>) =>
value ? TransferTypeMapping[value] : '',
},
{
headerName: t('Quantity'),
field: 'quantity',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Asset'),
field: 'assetId',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
data?.asset?.symbol || '',
},
{
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Receiver account balance'),
field: 'toAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Vega time'),
field: 'vegaTime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
filterParams: dateRangeFilterParams,
filter: DateRangeFilter,
},
],
[]
);
return (
<AgGrid
tooltipShowDelay={500}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
{...props}
/>
);
};
@@ -1,8 +1,16 @@
import { isNumeric } from '@vegaprotocol/utils';
import { PriceChangeCell } from '@vegaprotocol/datagrid';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
isNumeric,
priceChange,
priceChangePercentage,
} from '@vegaprotocol/utils';
import { PriceChangeCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useCandles } from '../../hooks/use-candles';
import BigNumber from 'bignumber.js';
import classNames from 'classnames';
interface Props {
marketId?: string;
@@ -47,10 +55,39 @@ export const Last24hPriceChange = ({
if (error || !isNumeric(decimalPlaces)) {
return <span>-</span>;
}
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles);
return (
<PriceChangeCell
candles={oneDayCandles?.map((c) => c.close) || initialValue || []}
decimalPlaces={decimalPlaces}
/>
<span
className={classNames(
'flex items-center gap-1',
signedNumberCssClass(change)
)}
>
<Arrow value={change} />
<span data-testid="price-change-percentage">
{formatNumberPercentage(new BigNumber(changePercentage.toString()), 2)}
</span>
<span data-testid="price-change">
{addDecimalsFormatNumber(change.toString(), decimalPlaces ?? 0, 3)}
</span>
</span>
);
};
const Arrow = ({ value }: { value: number | bigint }) => {
const size = 10;
if (value > 0) {
return <VegaIcon name={VegaIconNames.ARROW_UP} size={size} />;
}
if (value < 0) {
return <VegaIcon name={VegaIconNames.ARROW_DOWN} size={size} />;
}
return null;
};
@@ -98,7 +98,7 @@ export const Row = ({
<div style={{ wordBreak: 'break-word' }}>
{valueDiffersFromParentMarket ? (
<div className="flex items-center gap-3">
<span className="line-through">
<span className="line-through dark:text-vega-dark-300">
{getFormattedValue(parentValue)}
</span>
<span>{formattedValue}</span>
@@ -1,13 +1,14 @@
import isEqual from 'lodash/isEqual';
import type { ReactNode } from 'react';
import { Fragment, useState } from 'react';
import { useMemo } from 'react';
import { Fragment, useMemo, useState } from 'react';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { marketDataProvider } from '../../market-data-provider';
import { totalFeesPercentage } from '../../market-utils';
import {
ExternalLink,
Intent,
Lozenge,
Splash,
Tooltip,
VegaIcon,
@@ -27,9 +28,15 @@ import type {
} from './market-info-data-provider';
import { Last24hVolume } from '../last-24h-volume';
import BigNumber from 'bignumber.js';
import type { DataSourceDefinition, SignerKind } from '@vegaprotocol/types';
import { ConditionOperatorMapping } from '@vegaprotocol/types';
import { MarketTradingModeMapping } from '@vegaprotocol/types';
import type {
DataSourceDefinition,
MarketTradingMode,
SignerKind,
} from '@vegaprotocol/types';
import {
ConditionOperatorMapping,
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import {
DApp,
FLAGS,
@@ -48,8 +55,6 @@ import {
useSuccessorMarketQuery,
} from '../../__generated__';
import { useSuccessorMarketProposalDetailsQuery } from '@vegaprotocol/proposals';
import type { MarketTradingMode } from '@vegaprotocol/types';
import type { Signer } from '@vegaprotocol/types';
import classNames from 'classnames';
import compact from 'lodash/compact';
@@ -575,7 +580,6 @@ export const RiskFactorsInfoPanel = ({
export const PriceMonitoringBoundsInfoPanel = ({
market,
triggerIndex,
parentMarket,
}: MarketInfoProps & {
triggerIndex: number;
}) => {
@@ -584,33 +588,13 @@ export const PriceMonitoringBoundsInfoPanel = ({
variables: { marketId: market.id },
});
const { data: parentData } = useDataProvider({
dataProvider: marketDataProvider,
variables: { marketId: parentMarket?.id || '' },
skip:
!parentMarket ||
!parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[
triggerIndex
],
});
const quoteUnit =
market?.tradableInstrument.instrument.product?.quoteName || '';
const parentQuoteUnit =
parentMarket?.tradableInstrument.instrument.product?.quoteName || '';
const isParentQuoteUnitEqual = quoteUnit === parentQuoteUnit;
const trigger =
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const parentTrigger =
parentMarket?.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
const isParentTriggerEqual = isEqual(trigger, parentTrigger);
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
const parentBounds = parentData?.priceMonitoringBounds?.[triggerIndex];
const shouldShowParentData =
isParentQuoteUnitEqual && isParentTriggerEqual && !!parentBounds;
if (!trigger) {
console.error(
@@ -638,14 +622,6 @@ export const PriceMonitoringBoundsInfoPanel = ({
highestPrice: bounds.maxValidPrice,
lowestPrice: bounds.minValidPrice,
}}
parentData={
shouldShowParentData
? {
highestPrice: parentBounds.maxValidPrice,
lowestPrice: parentBounds.minValidPrice,
}
: undefined
}
decimalPlaces={market.decimalPlaces}
assetSymbol={quoteUnit}
/>
@@ -839,45 +815,76 @@ export const OracleInfoPanel = ({
: (parentProduct?.dataSourceSpecForTradingTermination
?.data as DataSourceDefinition);
const isParentDataSourceSpecEqual =
parentDataSourceSpec !== undefined &&
dataSourceSpec === parentDataSourceSpec;
const isParentDataSourceSpecIdEqual =
const shouldShowParentData =
parentMarket !== undefined &&
parentDataSourceSpecId !== undefined &&
dataSourceSpecId === parentDataSourceSpecId;
!isEqual(dataSourceSpec, parentDataSourceSpec);
const wrapperClasses = classNames('mb-4', {
'flex items-center gap-6': shouldShowParentData,
});
// We'll only provide successor parent data (if it differs) to the
// DataSourceProof component. Having an old external link struck through
// is unlikely to be useful.
return (
<div className="flex flex-col gap-2">
<DataSourceProof
data-testid="oracle-proof-links"
data={dataSourceSpec}
providers={data}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentData={
isParentDataSourceSpecEqual ? undefined : parentDataSourceSpec
}
parentDataSourceSpecId={
isParentDataSourceSpecIdEqual ? undefined : parentDataSourceSpecId
}
/>
<>
{shouldShowParentData && (
<Lozenge variant={Intent.Primary} className="text-sm">
{t('Updated')}
</Lozenge>
)}
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
type === 'settlementData'
? product.dataSourceSpecForSettlementData.id
: product.dataSourceSpecForTradingTermination.id
}`}
>
{type === 'settlementData'
? t('View settlement data specification')
: t('View termination specification')}
</ExternalLink>
</div>
<div className={wrapperClasses}>
{shouldShowParentData &&
parentDataSourceSpec &&
parentDataSourceSpecId &&
parentProduct && (
<div className="flex flex-col gap-2 text-vega-dark-300 line-through">
<DataSourceProof
data-testid="oracle-proof-links"
data={parentDataSourceSpec}
providers={data}
type={type}
dataSourceSpecId={parentDataSourceSpecId}
/>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
type === 'settlementData'
? parentProduct.dataSourceSpecForSettlementData.id
: parentProduct.dataSourceSpecForTradingTermination.id
}`}
>
{type === 'settlementData'
? t('View settlement data specification')
: t('View termination specification')}
</ExternalLink>
</div>
)}
<div className="flex flex-col gap-2">
<DataSourceProof
data-testid="oracle-proof-links"
data={dataSourceSpec}
providers={data}
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
<ExternalLink
data-testid="oracle-spec-links"
href={`${VEGA_EXPLORER_URL}/oracles/${
type === 'settlementData'
? product.dataSourceSpecForSettlementData.id
: product.dataSourceSpecForTradingTermination.id
}`}
>
{type === 'settlementData'
? t('View settlement data specification')
: t('View termination specification')}
</ExternalLink>
</div>
</div>
</>
);
};
@@ -886,28 +893,14 @@ export const DataSourceProof = ({
providers,
type,
dataSourceSpecId,
parentData,
parentDataSourceSpecId,
}: {
data: DataSourceDefinition;
providers: Provider[] | undefined;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentData?: DataSourceDefinition;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, we'll only pass parent data to child
// components for comparison if the data differs from the parent market.
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
let parentSigners: Signer[];
if (
parentData &&
parentData.sourceType.__typename === 'DataSourceDefinitionExternal'
) {
parentSigners = parentData.sourceType.sourceType?.signers || [];
}
if (!providers?.length) {
return <NoOracleProof type={type} />;
@@ -915,34 +908,15 @@ export const DataSourceProof = ({
return (
<div className="flex flex-col gap-2">
{signers.map(({ signer }, i) => {
const parentSigner = parentSigners?.find(
({ signer: ParentSigner }) =>
ParentSigner.__typename === signer.__typename
)?.signer;
const isParentSignerEqual = isEqual(signer, parentSigner);
return isParentSignerEqual ? (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
parentSigner={parentSigner}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
{signers.map(({ signer }, i) => (
<OracleLink
key={i}
providers={providers}
signer={signer}
type={type}
dataSourceSpecId={dataSourceSpecId}
/>
))}
</div>
);
}
@@ -1002,22 +976,13 @@ const OracleLink = ({
signer,
type,
dataSourceSpecId,
parentSigner,
parentDataSourceSpecId,
}: {
providers: Provider[];
signer: SignerKind;
type: 'settlementData' | 'termination';
dataSourceSpecId: string;
parentSigner?: SignerKind;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const signerProviders = getSignerProviders(signer, providers);
const parentSignerProviders = parentSigner
? getSignerProviders(parentSigner, providers)
: [];
if (!signerProviders.length) {
return <NoOracleProof type={type} />;
@@ -1025,34 +990,13 @@ const OracleLink = ({
return (
<div className="mt-2">
{signerProviders.map((provider) => {
// Making the assumption here that if the provider name is the same,
// that it is the same provider that the parent market used.
const parentProvider = parentSignerProviders.find(
(p) => p.name === provider.name
);
const isParentProviderEqual =
parentProvider !== undefined && isEqual(provider, parentProvider);
// We only want to pass the parent data to the child component if the
// data differs from the parent market.
return isParentProviderEqual ? (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
) : (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
parentProvider={parentProvider}
parentDataSourceSpecId={parentDataSourceSpecId}
/>
);
})}
{signerProviders.map((provider) => (
<OracleProfile
key={dataSourceSpecId}
provider={provider}
dataSourceSpecId={dataSourceSpecId}
/>
))}
</div>
);
};
@@ -1075,18 +1019,13 @@ const NoOracleProof = ({
const OracleProfile = (props: {
provider: Provider;
dataSourceSpecId: string;
parentProvider?: Provider;
parentDataSourceSpecId?: string;
}) => {
// If this is a successor market, the parent market data will only have been passed
// in if it differs from the current data.
const [open, onChange] = useState(false);
return (
<div key={props.provider.name}>
<OracleBasicProfile
provider={props.provider}
onClick={() => onChange(!open)}
parentProvider={props.parentProvider}
/>
<OracleDialog {...props} open={open} onChange={onChange} />
</div>

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