Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0cc27401 | ||
|
|
9ac199f59a | ||
|
|
332cc302c3 | ||
|
|
4da0e9c368 | ||
|
|
13e1f99767 | ||
|
|
5c18c898b0 | ||
|
|
4fe81cc4aa | ||
|
|
0fb8ee3abb | ||
|
|
3422b99491 | ||
|
|
287e294281 | ||
|
|
dc959025c6 |
@@ -1,6 +1,6 @@
|
||||
# Related issues 🔗
|
||||
|
||||
Closes #[Issue number here]
|
||||
Issue: #[Issue number here]
|
||||
|
||||
# Description ℹ️
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
'-'
|
||||
)}
|
||||
<i className="text-xs">@</i>
|
||||
<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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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)';
|
||||
|
||||
@@ -29,3 +29,4 @@ LC_ALL="en_US.UTF-8"
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=true
|
||||
@@ -30,3 +30,4 @@ CYPRESS_FAIRGROUND=false
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_METAMASK_SNAPS=false
|
||||
@@ -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
|
||||
@@ -22,3 +22,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_METAMASK_SNAPS=false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,3 +20,4 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
NX_METAMASK_SNAPS=false
|
||||
|
||||
@@ -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,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,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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,3 +21,4 @@ NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
# NX_ICEBERG_ORDERS
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
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';
|
||||
@@ -35,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';
|
||||
@@ -632,11 +633,11 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
/>
|
||||
<Size control={control} sizeStep={sizeStep} />
|
||||
<TimeInForce control={control} />
|
||||
<div className="flex gap-2 pb-3 justify-end">
|
||||
<div className="flex justify-end pb-3 gap-2">
|
||||
<ReduceOnly />
|
||||
</div>
|
||||
<hr className="mb-4 border-vega-clight-500 dark:border-vega-cdark-500" />
|
||||
<div className="flex gap-2 pb-2 justify-between">
|
||||
<div className="flex justify-between pb-2 gap-2">
|
||||
<Controller
|
||||
name="oco"
|
||||
control={control}
|
||||
@@ -713,7 +714,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
/>
|
||||
<Size control={control} sizeStep={sizeStep} oco />
|
||||
<TimeInForce control={control} oco />
|
||||
<div className="flex gap-2 mb-2 justify-end">
|
||||
<div className="flex justify-end mb-2 gap-2">
|
||||
<ReduceOnly />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -38,8 +38,6 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||
import type { OrderInfo } from '@vegaprotocol/types';
|
||||
|
||||
import {
|
||||
validateExpiration,
|
||||
validateMarketState,
|
||||
@@ -59,8 +57,6 @@ import {
|
||||
useMarketAccountBalance,
|
||||
useAccountBalance,
|
||||
} from '@vegaprotocol/accounts';
|
||||
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import {
|
||||
DealTicketType,
|
||||
@@ -71,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.';
|
||||
@@ -230,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,
|
||||
@@ -239,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,
|
||||
@@ -307,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) => {
|
||||
@@ -468,6 +463,8 @@ export const DealTicket = ({
|
||||
value={field.value}
|
||||
orderType={type}
|
||||
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())
|
||||
@@ -476,6 +473,12 @@ export const DealTicket = ({
|
||||
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}
|
||||
@@ -502,7 +505,7 @@ 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}
|
||||
@@ -568,7 +571,7 @@ 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}
|
||||
@@ -577,6 +580,7 @@ export const DealTicket = ({
|
||||
name="iceberg"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={disableIcebergCheckbox}
|
||||
label={
|
||||
<Tooltip
|
||||
description={
|
||||
|
||||
@@ -18,6 +18,8 @@ export const ExpirySelector = ({
|
||||
onSelect,
|
||||
errorMessage,
|
||||
}: ExpirySelectorProps) => {
|
||||
const minDateRef = useRef(new Date());
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<TradingFormGroup
|
||||
@@ -31,7 +33,7 @@ export const ExpirySelector = ({
|
||||
type="datetime-local"
|
||||
value={value && formatForInput(new Date(value))}
|
||||
onChange={(e) => onSelect(e.target.value)}
|
||||
min={formatForInput(useRef(new Date()).current)}
|
||||
min={formatForInput(minDateRef.current)}
|
||||
hasError={!!errorMessage}
|
||||
/>
|
||||
{errorMessage && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 +1 @@
|
||||
export * from './lib/ledger-manager';
|
||||
export * from './lib/__generated__/LedgerEntries';
|
||||
export * from './lib/ledger-export-form';
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useTimeToUpgrade } from './use-time-to-upgrade';
|
||||
import {
|
||||
ERR_NO_TIME_UNITS,
|
||||
parseDuration,
|
||||
useTimeToUpgrade,
|
||||
} from './use-time-to-upgrade';
|
||||
|
||||
jest.mock('./__generated__/BlockStatistics', () => ({
|
||||
...jest.requireActual('./__generated__/BlockStatistics'),
|
||||
@@ -8,7 +12,7 @@ jest.mock('./__generated__/BlockStatistics', () => ({
|
||||
data: {
|
||||
statistics: {
|
||||
blockHeight: 1,
|
||||
blockDuration: 500,
|
||||
blockDuration: '500ms',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -30,3 +34,25 @@ describe('useTimeToUpgrade', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDuration', () => {
|
||||
it.each([
|
||||
['1000000ns', 1],
|
||||
['1000µs', 1],
|
||||
['1ms', 1],
|
||||
['1s', 1000],
|
||||
['1m', 60 * 1000],
|
||||
['1h', 60 * 60 * 1000],
|
||||
// below test cases are from vega
|
||||
['3.3s', 3300],
|
||||
['4m5s', 4 * 60 * 1000 + 5 * 1000],
|
||||
['4m5.001s', 4 * 60 * 1000 + 5001],
|
||||
['5h6m7.001s', 5 * 60 * 60 * 1000 + 6 * 60 * 1000 + 7001],
|
||||
['8m0.000000001s', 8 * 60 * 1000 + 1 / 1000000],
|
||||
])('parses %s to %d milliseconds', (input, output) => {
|
||||
expect(parseDuration(input)).toEqual(output);
|
||||
});
|
||||
it('throws an error when given corrupted data', () => {
|
||||
expect(() => parseDuration('blah')).toThrow(ERR_NO_TIME_UNITS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,52 @@ const DEFAULT_POLLS = 10;
|
||||
const INTERVAL = 1000;
|
||||
const durations = [] as number[];
|
||||
|
||||
export const ERR_NO_TIME_UNITS = new Error(
|
||||
'could not parse block duration value - no time units detected'
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses block duration value and output a number of milliseconds.
|
||||
* @param input The block duration input from the API, e.g. 4m5.001s
|
||||
* @returns A number of milliseconds
|
||||
*/
|
||||
export const parseDuration = (input: string) => {
|
||||
// h -> 60*60*1000
|
||||
// m -> 60*1000
|
||||
// s -> 1000
|
||||
// ms -> 1
|
||||
// µs -> 1/1000
|
||||
// ns -> 1/1000000
|
||||
let H = 0;
|
||||
let M = 0;
|
||||
let S = 0;
|
||||
const lessThanSecond = /^[0-9.]+[nµm]*s$/gu.test(input);
|
||||
const exp = /(?<hours>[0-9.]+h)?(?<minutes>[0-9.]+m)?(?<seconds>[0-9.]+s)?/gu;
|
||||
const m = exp.exec(input);
|
||||
|
||||
const hours = m?.groups?.['hours'];
|
||||
const minutes = m?.groups?.['minutes'];
|
||||
const seconds = lessThanSecond ? input : m?.groups?.['seconds'];
|
||||
if (!lessThanSecond && !hours && !minutes && !seconds) {
|
||||
throw ERR_NO_TIME_UNITS;
|
||||
}
|
||||
|
||||
if (seconds) {
|
||||
S = parseFloat(seconds);
|
||||
if (seconds.includes('ns')) S /= 1000 * 1000;
|
||||
else if (seconds.includes('µs')) S /= 1000;
|
||||
else if (seconds.includes('ms')) S *= 1;
|
||||
else if (seconds.includes('s')) S *= 1000;
|
||||
}
|
||||
if (minutes && !lessThanSecond) {
|
||||
M = parseFloat(minutes) * 60 * 1000;
|
||||
}
|
||||
if (hours && !lessThanSecond) {
|
||||
H = parseFloat(hours) * 60 * 60 * 1000;
|
||||
}
|
||||
return H + M + S;
|
||||
};
|
||||
|
||||
const useAverageBlockDuration = (polls = DEFAULT_POLLS) => {
|
||||
const [avg, setAvg] = useState<number | undefined>(undefined);
|
||||
const { data, startPolling, stopPolling, error } = useBlockStatisticsQuery({
|
||||
@@ -28,7 +74,11 @@ const useAverageBlockDuration = (polls = DEFAULT_POLLS) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (durations.length < polls && data) {
|
||||
durations.push(parseFloat(data.statistics.blockDuration));
|
||||
try {
|
||||
durations.push(parseDuration(data.statistics.blockDuration)); // ms
|
||||
} catch (err) {
|
||||
// NOOP - do not add unparsed value to AVG
|
||||
}
|
||||
}
|
||||
if (durations.length === polls) {
|
||||
const averageBlockDuration = sum(durations) / durations.length; // ms
|
||||
|
||||
@@ -177,7 +177,21 @@ module.exports = {
|
||||
success: '#00F780',
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['Roboto Mono', 'monospace'],
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Cascadia Mono',
|
||||
'Segoe UI Mono',
|
||||
'Roboto Mono',
|
||||
'Oxygen Mono',
|
||||
'Ubuntu Monospace',
|
||||
'Source Code Pro',
|
||||
'Fira Mono',
|
||||
'Droid Sans Mono',
|
||||
'Courier New',
|
||||
'monospace',
|
||||
],
|
||||
sans: [
|
||||
'"Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
||||
],
|
||||
|
||||
Generated
+2
@@ -4414,6 +4414,8 @@ export type StopOrderFilter = {
|
||||
dateRange?: InputMaybe<DateRange>;
|
||||
/** Zero or more expiry strategies to filter by */
|
||||
expiryStrategy?: InputMaybe<Array<StopOrderExpiryStrategy>>;
|
||||
/** Filter for live stop orders only */
|
||||
liveOnly?: InputMaybe<Scalars['Boolean']>;
|
||||
/** Zero or more market IDs to filter by */
|
||||
markets?: InputMaybe<Array<Scalars['ID']>>;
|
||||
/** Zero or more party IDs to filter by */
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
export const IconMetaMask = ({ size = 16 }: { size: number }) => (
|
||||
<svg viewBox="0 0 47 47" fill="none" height={size}>
|
||||
<g>
|
||||
<path
|
||||
d="m40.632 6.969-14.136 10.62 2.628-6.259L40.632 6.97Z"
|
||||
fill="#E17726"
|
||||
stroke="#E17726"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m8.024 6.969 14.01 10.72-2.502-6.359L8.024 6.97ZM35.542 31.594l-3.761 5.834 8.054 2.251 2.307-7.958-6.6-.127ZM6.528 31.721 8.82 39.68l8.04-2.251-3.747-5.834-6.586.127Z"
|
||||
fill="#E27625"
|
||||
stroke="#E27625"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m16.428 21.738-2.237 3.427 7.97.368-.266-8.709-5.467 4.914ZM32.229 21.738l-5.552-5.012-.181 8.807 7.97-.368-2.237-3.427ZM16.861 37.428l4.824-2.365-4.152-3.285-.672 5.65ZM26.971 35.063l4.81 2.365-.657-5.65-4.153 3.285Z"
|
||||
fill="#E27625"
|
||||
stroke="#E27625"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m31.78 37.428-4.81-2.365.392 3.172-.042 1.345 4.46-2.152ZM16.861 37.428l4.475 2.152-.028-1.345.377-3.172-4.824 2.365Z"
|
||||
fill="#D5BFB2"
|
||||
stroke="#D5BFB2"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m21.42 29.682-4-1.19 2.825-1.316 1.174 2.506ZM27.236 29.682l1.175-2.506 2.838 1.317-4.013 1.19Z"
|
||||
fill="#233447"
|
||||
stroke="#233447"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m16.861 37.427.7-5.834-4.447.128 3.747 5.706ZM31.096 31.593l.685 5.834 3.761-5.706-4.446-.128ZM34.465 25.165l-7.97.368.741 4.15 1.175-2.507 2.838 1.317 3.216-3.328ZM17.42 28.493l2.825-1.317 1.175 2.506.74-4.149-7.97-.368 3.23 3.328Z"
|
||||
fill="#CC6228"
|
||||
stroke="#CC6228"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m14.19 25.165 3.343 6.613-.112-3.285-3.23-3.328ZM31.25 28.493l-.126 3.285 3.342-6.613-3.216 3.328ZM22.161 25.533l-.741 4.149.937 4.9.21-6.458-.406-2.591ZM26.495 25.533l-.391 2.577.196 6.471.937-4.9-.741-4.148Z"
|
||||
fill="#E27525"
|
||||
stroke="#E27525"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m27.237 29.682-.937 4.9.671.481 4.153-3.285.126-3.285-4.013 1.19ZM17.42 28.493l.112 3.285 4.153 3.285.671-.481-.937-4.9-3.999-1.19Z"
|
||||
fill="#F5841F"
|
||||
stroke="#F5841F"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m27.32 39.58.042-1.345-.363-.312h-5.342l-.35.312.029 1.345-4.475-2.152 1.566 1.303 3.175 2.223h5.439l3.188-2.224 1.552-1.302-4.46 2.152Z"
|
||||
fill="#C0AC9D"
|
||||
stroke="#C0AC9D"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m26.97 35.063-.67-.482h-3.944l-.67.482-.378 3.172.35-.312h5.34l.364.312-.391-3.172Z"
|
||||
fill="#161616"
|
||||
stroke="#161616"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m41.234 18.283 1.188-5.863-1.79-5.451-13.66 10.266 5.257 4.503 7.425 2.195 1.636-1.94-.713-.524 1.132-1.048-.867-.68 1.133-.878-.741-.58ZM6.234 12.42l1.203 5.863-.77.58 1.147.878-.867.68L8.08 21.47l-.713.524 1.636 1.94 7.425-2.195 5.257-4.503L8.025 6.97l-1.79 5.452Z"
|
||||
fill="#763E1A"
|
||||
stroke="#763E1A"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m39.654 23.933-7.425-2.195 2.237 3.427-3.342 6.613 4.419-.057h6.6l-2.49-7.788ZM16.428 21.738l-7.425 2.195-2.475 7.788h6.586l4.418.056-3.342-6.612 2.238-3.427ZM26.495 25.533l.476-8.298 2.153-5.905h-9.592l2.153 5.905.476 8.298.181 2.605.014 6.443H26.3l.014-6.443.182-2.605Z"
|
||||
fill="#F5841F"
|
||||
stroke="#F5841F"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IconArrowDown } from './svg-icons/icon-arrow-down';
|
||||
import { IconArrowLeft } from './svg-icons/icon-arrow-left';
|
||||
import { IconArrowUp } from './svg-icons/icon-arrow-up';
|
||||
import { IconArrowRight } from './svg-icons/icon-arrow-right';
|
||||
import { IconArrowTopRight } from './svg-icons/icon-arrow-top-right';
|
||||
import { IconArrowUp } from './svg-icons/icon-arrow-up';
|
||||
import { IconBreakdown } from './svg-icons/icon-breakdown';
|
||||
import { IconBullet } from './svg-icons/icon-bullet';
|
||||
import { IconChevronDown } from './svg-icons/icon-chevron-down';
|
||||
@@ -20,28 +20,29 @@ import { IconGlobe } from './svg-icons/icon-globe';
|
||||
import { IconInfo } from './svg-icons/icon-info';
|
||||
import { IconKebab } from './svg-icons/icon-kebab';
|
||||
import { IconLinkedIn } from './svg-icons/icon-linkedin';
|
||||
import { IconMetaMask } from './svg-icons/icon-metamask';
|
||||
import { IconMinus } from './svg-icons/icon-minus';
|
||||
import { IconMoon } from './svg-icons/icon-moon';
|
||||
import { IconOpenExternal } from './svg-icons/icon-open-external';
|
||||
import { IconQuestionMark } from './svg-icons/icon-question-mark';
|
||||
import { IconPlus } from './svg-icons/icon-plus';
|
||||
import { IconQuestionMark } from './svg-icons/icon-question-mark';
|
||||
import { IconSearch } from './svg-icons/icon-search';
|
||||
import { IconStar } from './svg-icons/icon-star';
|
||||
import { IconTick } from './svg-icons/icon-tick';
|
||||
import { IconTicket } from './svg-icons/icon-ticket';
|
||||
import { IconTransfer } from './svg-icons/icon-transfer';
|
||||
import { IconTrendUp } from './svg-icons/icon-trend-up';
|
||||
import { IconTrendDown } from './svg-icons/icon-trend-down';
|
||||
import { IconTrendUp } from './svg-icons/icon-trend-up';
|
||||
import { IconTwitter } from './svg-icons/icon-twitter';
|
||||
import { IconVote } from './svg-icons/icon-vote';
|
||||
import { IconWithdraw } from './svg-icons/icon-withdraw';
|
||||
import { IconSearch } from './svg-icons/icon-search';
|
||||
|
||||
export enum VegaIconNames {
|
||||
ARROW_DOWN = 'arrow-down',
|
||||
ARROW_LEFT = 'arrow-left',
|
||||
ARROW_UP = 'arrow-up',
|
||||
ARROW_RIGHT = 'arrow-right',
|
||||
ARROW_TOP_RIGHT = 'arrow-top-right',
|
||||
ARROW_UP = 'arrow-up',
|
||||
BREAKDOWN = 'breakdown',
|
||||
BULLET = 'bullet',
|
||||
CHEVRON_DOWN = 'chevron-down',
|
||||
@@ -59,18 +60,19 @@ export enum VegaIconNames {
|
||||
INFO = 'info',
|
||||
KEBAB = 'kebab',
|
||||
LINKEDIN = 'linkedin',
|
||||
METAMASK = 'metamask',
|
||||
MINUS = 'minus',
|
||||
MOON = 'moon',
|
||||
OPEN_EXTERNAL = 'open-external',
|
||||
QUESTION_MARK = 'question-mark',
|
||||
PLUS = 'plus',
|
||||
QUESTION_MARK = 'question-mark',
|
||||
SEARCH = 'search',
|
||||
STAR = 'star',
|
||||
TICK = 'tick',
|
||||
TICKET = 'ticket',
|
||||
TRANSFER = 'transfer',
|
||||
TREND_UP = 'trend-up',
|
||||
TREND_DOWN = 'trend-down',
|
||||
TREND_UP = 'trend-up',
|
||||
TWITTER = 'twitter',
|
||||
VOTE = 'vote',
|
||||
WITHDRAW = 'withdraw',
|
||||
@@ -82,38 +84,39 @@ export const VegaIconNameMap: Record<
|
||||
> = {
|
||||
'arrow-down': IconArrowDown,
|
||||
'arrow-left': IconArrowLeft,
|
||||
'arrow-up': IconArrowUp,
|
||||
'arrow-right': IconArrowRight,
|
||||
'arrow-top-right': IconArrowTopRight,
|
||||
breakdown: IconBreakdown,
|
||||
bullet: IconBullet,
|
||||
'arrow-up': IconArrowUp,
|
||||
'chevron-down': IconChevronDown,
|
||||
'chevron-left': IconChevronLeft,
|
||||
'chevron-up': IconChevronUp,
|
||||
'exclaimation-mark': IconExclaimationMark,
|
||||
'open-external': IconOpenExternal,
|
||||
'question-mark': IconQuestionMark,
|
||||
'trend-down': IconTrendDown,
|
||||
'trend-up': IconTrendUp,
|
||||
breakdown: IconBreakdown,
|
||||
bullet: IconBullet,
|
||||
cog: IconCog,
|
||||
copy: IconCopy,
|
||||
cross: IconCross,
|
||||
deposit: IconDeposit,
|
||||
edit: IconEdit,
|
||||
'exclaimation-mark': IconExclaimationMark,
|
||||
eye: IconEye,
|
||||
forum: IconForum,
|
||||
globe: IconGlobe,
|
||||
info: IconInfo,
|
||||
kebab: IconKebab,
|
||||
linkedin: IconLinkedIn,
|
||||
metamask: IconMetaMask,
|
||||
minus: IconMinus,
|
||||
moon: IconMoon,
|
||||
'open-external': IconOpenExternal,
|
||||
plus: IconPlus,
|
||||
'question-mark': IconQuestionMark,
|
||||
search: IconSearch,
|
||||
star: IconStar,
|
||||
tick: IconTick,
|
||||
ticket: IconTicket,
|
||||
transfer: IconTransfer,
|
||||
'trend-up': IconTrendUp,
|
||||
'trend-down': IconTrendDown,
|
||||
twitter: IconTwitter,
|
||||
vote: IconVote,
|
||||
withdraw: IconWithdraw,
|
||||
|
||||
@@ -68,7 +68,11 @@ export const ShowMore = ({
|
||||
|
||||
{!expanded && (
|
||||
<div className="mt-1 text-center">
|
||||
<Button size={'sm'} onClick={() => setExpanded(true)}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={() => setExpanded(true)}
|
||||
data-testid="show-more-btn"
|
||||
>
|
||||
{t('Show more')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,9 @@ export const ethereumAddress = (value: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
|
||||
export const vegaPublicKey = (value: string) => {
|
||||
if (!/^[A-Fa-f0-9]{64}$/i.test(value)) {
|
||||
if (!VEGA_ID_REGEX.test(value)) {
|
||||
return t('Invalid Vega key');
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -16,13 +16,16 @@ import type { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { VegaConnector } from '../connectors';
|
||||
import {
|
||||
DEFAULT_SNAP_ID,
|
||||
InjectedConnector,
|
||||
JsonRpcConnector,
|
||||
SnapConnector,
|
||||
ViewConnector,
|
||||
requestSnap,
|
||||
} from '../connectors';
|
||||
import { JsonRpcConnectorForm } from './json-rpc-connector-form';
|
||||
import { ViewConnectorForm } from './view-connector-form';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
BrowserIcon,
|
||||
ConnectDialogContent,
|
||||
@@ -38,10 +41,11 @@ import { useVegaWallet } from '../use-vega-wallet';
|
||||
import { InjectedConnectorForm } from './injected-connector-form';
|
||||
import { isBrowserWalletInstalled } from '../utils';
|
||||
import { useIsWalletServiceRunning } from '../use-is-wallet-service-running';
|
||||
import { useIsSnapRunning } from '../use-is-snap-running';
|
||||
|
||||
export const CLOSE_DELAY = 1700;
|
||||
type Connectors = { [key: string]: VegaConnector };
|
||||
export type WalletType = 'injected' | 'jsonRpc' | 'view';
|
||||
export type WalletType = 'injected' | 'jsonRpc' | 'view' | 'snap';
|
||||
|
||||
export interface VegaConnectDialogProps {
|
||||
connectors: Connectors;
|
||||
@@ -156,7 +160,10 @@ const ConnectDialogContainer = ({
|
||||
// for rest because we need to show an authentication form
|
||||
if (connector instanceof JsonRpcConnector) {
|
||||
jsonRpcConnect(connector, appChainId);
|
||||
} else if (connector instanceof InjectedConnector) {
|
||||
} else if (
|
||||
connector instanceof InjectedConnector ||
|
||||
connector instanceof SnapConnector
|
||||
) {
|
||||
injectedConnect(connector, appChainId);
|
||||
}
|
||||
};
|
||||
@@ -166,6 +173,8 @@ const ConnectDialogContainer = ({
|
||||
appChainId
|
||||
);
|
||||
|
||||
const isSnapRunning = useIsSnapRunning(DEFAULT_SNAP_ID);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogContent>
|
||||
@@ -185,6 +194,7 @@ const ConnectDialogContainer = ({
|
||||
setWalletUrl={setWalletUrl}
|
||||
onSelect={handleSelect}
|
||||
isDesktopWalletRunning={isDesktopWalletRunning}
|
||||
isSnapRunning={isSnapRunning}
|
||||
/>
|
||||
)}
|
||||
</ConnectDialogContent>
|
||||
@@ -198,11 +208,13 @@ const ConnectorList = ({
|
||||
walletUrl,
|
||||
setWalletUrl,
|
||||
isDesktopWalletRunning,
|
||||
isSnapRunning,
|
||||
}: {
|
||||
onSelect: (type: WalletType) => void;
|
||||
walletUrl: string;
|
||||
setWalletUrl: (value: string) => void;
|
||||
isDesktopWalletRunning: boolean | null;
|
||||
isSnapRunning: boolean | null;
|
||||
}) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const title = isBrowserWalletInstalled()
|
||||
@@ -238,6 +250,45 @@ const ConnectorList = ({
|
||||
<GetWalletButton />
|
||||
)}
|
||||
</div>
|
||||
{FLAGS.METAMASK_SNAPS ? (
|
||||
<div>
|
||||
{isSnapRunning ? (
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
text={
|
||||
<>
|
||||
<div className="w-full h-full flex justify-center items-center gap-1 text-base">
|
||||
{t('Connect via Vega MetaMask Snap')}
|
||||
</div>
|
||||
<div className="absolute right-1 top-0 h-8 flex items-center">
|
||||
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
onSelect('snap');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ConnectionOption
|
||||
type="snap"
|
||||
text={
|
||||
<>
|
||||
<div className="w-full h-full flex justify-center items-center gap-1 text-base">
|
||||
{t('Install Vega MetaMask Snap')}
|
||||
</div>
|
||||
<div className="absolute right-1 top-0 h-8 flex items-center">
|
||||
<VegaIcon name={VegaIconNames.METAMASK} size={24} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
onClick={() => {
|
||||
requestSnap(DEFAULT_SNAP_ID);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<ConnectionOption
|
||||
type="view"
|
||||
@@ -282,7 +333,10 @@ const SelectedForm = ({
|
||||
onConnect: () => void;
|
||||
riskMessage?: ReactNode;
|
||||
}) => {
|
||||
if (connector instanceof InjectedConnector) {
|
||||
if (
|
||||
connector instanceof InjectedConnector ||
|
||||
connector instanceof SnapConnector
|
||||
) {
|
||||
return (
|
||||
<InjectedConnectorForm
|
||||
status={injectedState.status}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { setAcknowledged } from '../storage';
|
||||
import { useVegaWallet } from '../use-vega-wallet';
|
||||
import { InjectedConnectorErrors, SnapConnectorErrors } from '../connectors';
|
||||
|
||||
export const InjectedConnectorForm = ({
|
||||
status,
|
||||
@@ -20,7 +21,6 @@ export const InjectedConnectorForm = ({
|
||||
reset,
|
||||
error,
|
||||
}: {
|
||||
// connector: JsonRpcConnector;
|
||||
appChainId: string;
|
||||
status: Status;
|
||||
error: Error | null;
|
||||
@@ -109,7 +109,7 @@ export const InjectedConnectorForm = ({
|
||||
|
||||
const Center = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<div className="flex justify-center items-center my-6">{children}</div>
|
||||
<div className="flex items-center justify-center my-6">{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -131,22 +131,30 @@ const Error = ({
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error.message === 'Invalid chain') {
|
||||
if (error.message === InjectedConnectorErrors.INVALID_CHAIN.message) {
|
||||
title = t('Wrong network');
|
||||
text = t(
|
||||
'To complete your wallet connection, set your wallet network in your app to "%s".',
|
||||
appChainId
|
||||
);
|
||||
} else if (error.message === 'window.vega not found') {
|
||||
} else if (
|
||||
error.message === InjectedConnectorErrors.VEGA_UNDEFINED.message
|
||||
) {
|
||||
title = t('No wallet detected');
|
||||
text = t('Vega browser extension not installed');
|
||||
} else if (
|
||||
error.message === SnapConnectorErrors.ETHEREUM_UNDEFINED.message ||
|
||||
error.message === SnapConnectorErrors.NODE_ADDRESS_NOT_SET.message
|
||||
) {
|
||||
title = t('Snap failed');
|
||||
text = t('Could not connect to Vega MetaMask Snap');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectDialogTitle>{title}</ConnectDialogTitle>
|
||||
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
|
||||
<p className="mb-2 text-center first-letter:uppercase">{text}</p>
|
||||
{tryAgain}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './vega-connector';
|
||||
export * from './injected-connector';
|
||||
export * from './json-rpc-connector';
|
||||
export * from './snap-connector';
|
||||
export * from './vega-connector';
|
||||
export * from './view-connector';
|
||||
|
||||
@@ -41,6 +41,11 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export const InjectedConnectorErrors = {
|
||||
VEGA_UNDEFINED: new Error('window.vega not found'),
|
||||
INVALID_CHAIN: new Error('Invalid chain'),
|
||||
};
|
||||
|
||||
export class InjectedConnector implements VegaConnector {
|
||||
description = 'Connects using the Vega wallet browser extension';
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
WalletError,
|
||||
type PubKey,
|
||||
type Transaction,
|
||||
type VegaConnector,
|
||||
} from './vega-connector';
|
||||
import { clearConfig, setConfig } from '../storage';
|
||||
|
||||
type RequestArguments = {
|
||||
method: string;
|
||||
params?: unknown[] | object;
|
||||
};
|
||||
type WindowEthereumProvider = {
|
||||
isMetaMask: boolean;
|
||||
request<T = unknown>(args: RequestArguments): Promise<T>;
|
||||
};
|
||||
|
||||
export const SnapConnectorErrors = {
|
||||
ETHEREUM_UNDEFINED: new Error('MetaMask extension could not be found'),
|
||||
NODE_ADDRESS_NOT_SET: new Error('nodeAddress is not set'),
|
||||
SNAP_ID_NOT_SET: new Error('snapId is not set'),
|
||||
TRANSACTION_PARSE: new Error('could not parse transaction data'),
|
||||
};
|
||||
|
||||
const ethereumRequest = <T>(args: RequestArguments): Promise<T> => {
|
||||
// can't declare `EthereumProvider` here because of the conflict with
|
||||
// type definitions of `@web3-react`
|
||||
if (
|
||||
'ethereum' in window &&
|
||||
typeof window.ethereum === 'object' &&
|
||||
window.ethereum &&
|
||||
'request' in window.ethereum &&
|
||||
'isMetaMask' in window.ethereum &&
|
||||
window.ethereum.isMetaMask &&
|
||||
typeof window.ethereum.request === 'function'
|
||||
) {
|
||||
return (window.ethereum as WindowEthereumProvider).request<T>(args);
|
||||
}
|
||||
throw SnapConnectorErrors.ETHEREUM_UNDEFINED;
|
||||
};
|
||||
|
||||
export const LOCAL_SNAP_ID = 'local:http://localhost:8080';
|
||||
export const DEFAULT_SNAP_ID = 'npm:@vegaprotocol/snap';
|
||||
|
||||
type GetSnapsResponse = Record<string, Snap>;
|
||||
|
||||
type Snap = {
|
||||
id: string;
|
||||
initialPermissions?: Record<string, unknown>;
|
||||
version: string;
|
||||
enables: boolean;
|
||||
blocked: boolean;
|
||||
};
|
||||
|
||||
type InvokeSnapRequest = {
|
||||
method: string;
|
||||
params?: object;
|
||||
};
|
||||
|
||||
type SendTransactionResponse =
|
||||
| {
|
||||
transactionHash: string;
|
||||
receivedAt: string;
|
||||
sentAt: string;
|
||||
transaction?: {
|
||||
signature?: {
|
||||
value: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
| {
|
||||
error: Error & {
|
||||
code: number;
|
||||
data: unknown;
|
||||
};
|
||||
};
|
||||
type GetChainIdResponse = {
|
||||
chainID: string;
|
||||
};
|
||||
type ListKeysResponse = { keys: PubKey[] };
|
||||
|
||||
/**
|
||||
* Requests permission for a website to communicate with the specified snaps
|
||||
* and attempts to install them if they're not already installed.
|
||||
* If the installation of any snap fails, returns the error that caused the failure.
|
||||
* More informations here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_requestsnaps
|
||||
*/
|
||||
export const requestSnap = async (
|
||||
snapId: string,
|
||||
params: Record<'version' | string, unknown> = {}
|
||||
) => {
|
||||
try {
|
||||
await ethereumRequest({
|
||||
method: 'wallet_requestSnaps',
|
||||
params: {
|
||||
[snapId]: params,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// NOOP - rejected by user
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list of all installed snaps.
|
||||
* More information here: https://docs.metamask.io/snaps/reference/rpc-api/#wallet_getsnaps
|
||||
*/
|
||||
export const getSnaps = async (): Promise<GetSnapsResponse> => {
|
||||
return (await ethereumRequest({
|
||||
method: 'wallet_getSnaps',
|
||||
})) as GetSnapsResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the requested snap by `snapId` and an optional `version`
|
||||
*/
|
||||
export const getSnap = async (
|
||||
snapId: string,
|
||||
version?: string
|
||||
): Promise<Snap | undefined> => {
|
||||
try {
|
||||
const snaps = await getSnaps();
|
||||
return Object.values(snaps).find(
|
||||
(snap) => snap.id === snapId && (!version || snap.version === version)
|
||||
);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const invokeSnap = async <T>(
|
||||
snapId: string,
|
||||
request: InvokeSnapRequest
|
||||
) => {
|
||||
const req = {
|
||||
method: 'wallet_invokeSnap',
|
||||
params: {
|
||||
snapId,
|
||||
request,
|
||||
},
|
||||
};
|
||||
return await ethereumRequest<T>(req);
|
||||
};
|
||||
|
||||
export class SnapConnector implements VegaConnector {
|
||||
description = "Connects using Vega Protocol's MetaMask snap";
|
||||
snapId: string | undefined = undefined;
|
||||
nodeAddress: string | undefined = undefined;
|
||||
|
||||
constructor(nodeAddress?: string, snapId = DEFAULT_SNAP_ID) {
|
||||
this.nodeAddress = nodeAddress;
|
||||
this.snapId = snapId;
|
||||
}
|
||||
|
||||
async listKeys() {
|
||||
if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET;
|
||||
return await invokeSnap<ListKeysResponse>(this.snapId, {
|
||||
method: 'client.list_keys',
|
||||
});
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const res = await this.listKeys();
|
||||
setConfig({
|
||||
connector: 'snap',
|
||||
token: null, // no token required for snap
|
||||
url: null, // no url required for snap
|
||||
});
|
||||
return res?.keys;
|
||||
}
|
||||
|
||||
async sendTx(pubKey: string, transaction: Transaction) {
|
||||
if (!this.nodeAddress) throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET;
|
||||
if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET;
|
||||
|
||||
// This step is needed to strip the transaction object from any additional
|
||||
// properties, such as `__proto__`, etc.
|
||||
let txData = null;
|
||||
try {
|
||||
txData = JSON.parse(JSON.stringify(transaction));
|
||||
} catch (err) {
|
||||
throw SnapConnectorErrors.TRANSACTION_PARSE;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
method: 'client.send_transaction',
|
||||
params: {
|
||||
sendingMode: 'TYPE_SYNC',
|
||||
transaction: txData,
|
||||
publicKey: pubKey,
|
||||
networkEndpoints: [this.nodeAddress],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await invokeSnap<SendTransactionResponse>(
|
||||
this.snapId,
|
||||
payload
|
||||
);
|
||||
|
||||
if ('error' in result) {
|
||||
const { message, code, data } = result.error;
|
||||
throw new WalletError(
|
||||
message,
|
||||
code,
|
||||
typeof data === 'string' ? data : ''
|
||||
);
|
||||
}
|
||||
|
||||
if (!result?.transaction?.signature) {
|
||||
throw new Error('could not retrieve transaction siganture');
|
||||
}
|
||||
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
signature: result?.transaction?.signature?.value,
|
||||
receivedAt: result.receivedAt,
|
||||
sentAt: result.sentAt,
|
||||
};
|
||||
}
|
||||
|
||||
async getChainId(): Promise<GetChainIdResponse> {
|
||||
if (!this.nodeAddress) throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET;
|
||||
if (!this.snapId) throw SnapConnectorErrors.SNAP_ID_NOT_SET;
|
||||
|
||||
const response = await invokeSnap<GetChainIdResponse>(this.snapId, {
|
||||
method: 'client.get_chain_id',
|
||||
params: { networkEndpoints: [this.nodeAddress] },
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
clearConfig();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/utils';
|
||||
|
||||
interface ConnectorConfig {
|
||||
token: string | null;
|
||||
connector: 'injected' | 'jsonRpc' | 'view';
|
||||
connector: 'injected' | 'jsonRpc' | 'view' | 'snap';
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { SnapConnector } from './';
|
||||
import { useVegaWallet } from './';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { VegaConnector } from './connectors/vega-connector';
|
||||
import { getConfig } from './storage';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
export function useEagerConnect(Connectors: {
|
||||
[connector: string]: VegaConnector;
|
||||
}) {
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
const [connecting, setConnecting] = useState(true);
|
||||
const { connect, acknowledgeNeeded } = useVegaWallet();
|
||||
|
||||
@@ -36,6 +39,12 @@ export function useEagerConnect(Connectors: {
|
||||
// @ts-ignore only injected wallet has connectWallet method
|
||||
await injectedInstance.connectWallet();
|
||||
await connect(injectedInstance);
|
||||
} else if (cfg.connector === 'snap') {
|
||||
const snapInstance = Connectors[cfg.connector] as SnapConnector;
|
||||
if (VEGA_URL) {
|
||||
snapInstance.nodeAddress = new URL(VEGA_URL).origin;
|
||||
await connect(snapInstance);
|
||||
}
|
||||
} else {
|
||||
await connect(Connectors[cfg.connector]);
|
||||
}
|
||||
@@ -49,7 +58,7 @@ export function useEagerConnect(Connectors: {
|
||||
if (typeof window !== 'undefined') {
|
||||
attemptConnect();
|
||||
}
|
||||
}, [connect, Connectors, acknowledgeNeeded]);
|
||||
}, [connect, Connectors, acknowledgeNeeded, VEGA_URL]);
|
||||
|
||||
return connecting;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { InjectedConnector } from './connectors';
|
||||
import {
|
||||
InjectedConnectorErrors,
|
||||
SnapConnector,
|
||||
SnapConnectorErrors,
|
||||
} from './connectors';
|
||||
import { InjectedConnector } from './connectors';
|
||||
import { useVegaWallet } from './use-vega-wallet';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
export enum Status {
|
||||
Idle = 'Idle',
|
||||
@@ -15,24 +21,39 @@ export const useInjectedConnector = (onConnect: () => void) => {
|
||||
const { connect, acknowledgeNeeded } = useVegaWallet();
|
||||
const [status, setStatus] = useState(Status.Idle);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
|
||||
const attemptConnect = useCallback(
|
||||
async (connector: InjectedConnector, appChainId: string) => {
|
||||
async (
|
||||
connector: InjectedConnector | SnapConnector,
|
||||
appChainId: string
|
||||
) => {
|
||||
try {
|
||||
if (!('vega' in window)) {
|
||||
throw new Error('window.vega not found');
|
||||
if (connector instanceof InjectedConnector && !('vega' in window)) {
|
||||
throw InjectedConnectorErrors.VEGA_UNDEFINED;
|
||||
}
|
||||
if (connector instanceof SnapConnector) {
|
||||
if (!('ethereum' in window)) {
|
||||
throw SnapConnectorErrors.ETHEREUM_UNDEFINED;
|
||||
}
|
||||
if (!VEGA_URL) {
|
||||
throw SnapConnectorErrors.NODE_ADDRESS_NOT_SET;
|
||||
}
|
||||
connector.nodeAddress = new URL(VEGA_URL).origin;
|
||||
}
|
||||
|
||||
setStatus(Status.GettingChainId);
|
||||
|
||||
const { chainID } = await connector.getChainId();
|
||||
|
||||
if (chainID !== appChainId) {
|
||||
throw new Error('Invalid chain');
|
||||
throw InjectedConnectorErrors.INVALID_CHAIN;
|
||||
}
|
||||
|
||||
setStatus(Status.Connecting);
|
||||
await connector.connectWallet(); // authorize wallet
|
||||
if (connector instanceof InjectedConnector) {
|
||||
// extra step for injected connector - authorize wallet
|
||||
await connector.connectWallet();
|
||||
}
|
||||
await connect(connector); // connect with keys
|
||||
|
||||
if (acknowledgeNeeded) {
|
||||
@@ -50,7 +71,7 @@ export const useInjectedConnector = (onConnect: () => void) => {
|
||||
setStatus(Status.Error);
|
||||
}
|
||||
},
|
||||
[acknowledgeNeeded, connect, onConnect]
|
||||
[VEGA_URL, acknowledgeNeeded, connect, onConnect]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSnap } from './connectors';
|
||||
|
||||
const INTERVAL = 2_000;
|
||||
|
||||
export const useIsSnapRunning = (snapId: string) => {
|
||||
const [running, setRunning] = useState(false);
|
||||
useEffect(() => {
|
||||
const checkState = async () => {
|
||||
const snap = await getSnap(snapId);
|
||||
setRunning(!!snap);
|
||||
};
|
||||
const i = setInterval(() => {
|
||||
checkState();
|
||||
}, INTERVAL);
|
||||
checkState();
|
||||
return () => {
|
||||
clearInterval(i);
|
||||
};
|
||||
}, [snapId]);
|
||||
return running;
|
||||
};
|
||||
@@ -73,6 +73,7 @@ export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
||||
order,
|
||||
};
|
||||
set({ transactions: transactions.concat(transaction) });
|
||||
|
||||
return transaction.id;
|
||||
},
|
||||
update: (index: number, update: Partial<VegaStoredTxState>) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { VegaTransactionContentMap } from './vega-transaction-dialog';
|
||||
import { VegaTransactionDialog } from './vega-transaction-dialog';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transaction } from './connectors';
|
||||
import type { WalletError } from './connectors';
|
||||
import { WalletError } from './connectors';
|
||||
import { ClientErrors } from './connectors';
|
||||
|
||||
export interface DialogProps {
|
||||
@@ -44,7 +44,7 @@ export const initialState = {
|
||||
};
|
||||
|
||||
export const orderErrorResolve = (err: Error | unknown): Error => {
|
||||
if (err instanceof WalletClientError) {
|
||||
if (err instanceof WalletClientError || err instanceof WalletError) {
|
||||
return err;
|
||||
} else if (err instanceof WalletHttpError) {
|
||||
return ClientErrors.UNKNOWN;
|
||||
|
||||
@@ -758,11 +758,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
|
||||
const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
let label = t('Error occurred');
|
||||
let errorMessage = `${tx.error?.message} ${
|
||||
tx.error instanceof WalletError && tx.error?.data
|
||||
? `: ${tx.error?.data}`
|
||||
: ''
|
||||
}`;
|
||||
let errorMessage =
|
||||
tx.error instanceof WalletError
|
||||
? `${tx.error.title}: ${tx.error.data}`
|
||||
: tx.error?.message;
|
||||
|
||||
const reconnectVegaWallet = useReconnectVegaWallet();
|
||||
|
||||
const orderRejection = tx.order && getRejectionReason(tx.order);
|
||||
@@ -773,6 +773,7 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
const walletError =
|
||||
tx.error instanceof WalletError &&
|
||||
walletNoConnectionCodes.includes(tx.error.code);
|
||||
|
||||
if (orderRejection) {
|
||||
label = getOrderToastTitle(tx.order?.status) || t('Order rejected');
|
||||
errorMessage = t('Your order has been rejected because: %s', [
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Dialog,
|
||||
Intent,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { MetaMask } from '@web3-react/metamask';
|
||||
import { WalletConnect } from '@web3-react/walletconnect-v2';
|
||||
import { WalletConnect as WalletConnectLegacy } from '@web3-react/walletconnect';
|
||||
@@ -118,7 +123,7 @@ export const Web3ConnectUncontrolledDialog = () => {
|
||||
function getConnectorInfo(connector: Connector) {
|
||||
if (connector instanceof MetaMask) {
|
||||
return {
|
||||
icon: <MetaMaskIcon width={40} />,
|
||||
icon: <VegaIcon name={VegaIconNames.METAMASK} size={32} />,
|
||||
name: 'MetaMask',
|
||||
text: t('MetaMask'),
|
||||
alt: t('MetaMask, Brave or other injected web wallet'),
|
||||
@@ -126,14 +131,14 @@ function getConnectorInfo(connector: Connector) {
|
||||
}
|
||||
if (connector instanceof CoinbaseWallet) {
|
||||
return {
|
||||
icon: <CoinbaseWalletIcon width={40} />,
|
||||
icon: <CoinbaseWalletIcon width={32} />,
|
||||
name: 'CoinbaseWallet',
|
||||
text: t('Coinbase'),
|
||||
};
|
||||
}
|
||||
if (connector instanceof WalletConnect) {
|
||||
return {
|
||||
icon: <WalletConnectIcon width={40} />,
|
||||
icon: <WalletConnectIcon width={32} />,
|
||||
name: 'WalletConnect',
|
||||
text: t('WalletConnect'),
|
||||
alt: t('WalletConnect v2'),
|
||||
@@ -143,7 +148,7 @@ function getConnectorInfo(connector: Connector) {
|
||||
return {
|
||||
icon: (
|
||||
<WalletConnectIcon
|
||||
width={40}
|
||||
width={32}
|
||||
fillColor={theme.colors.vega.light[200]}
|
||||
/>
|
||||
),
|
||||
@@ -160,115 +165,6 @@ type IconProps = {
|
||||
height?: number;
|
||||
};
|
||||
|
||||
const MetaMaskIcon = ({ width, height }: IconProps) => (
|
||||
<svg
|
||||
viewBox="0 0 47 47"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="m40.632 6.969-14.136 10.62 2.628-6.259L40.632 6.97Z"
|
||||
fill="#E17726"
|
||||
stroke="#E17726"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m8.024 6.969 14.01 10.72-2.502-6.359L8.024 6.97ZM35.542 31.594l-3.761 5.834 8.054 2.251 2.307-7.958-6.6-.127ZM6.528 31.721 8.82 39.68l8.04-2.251-3.747-5.834-6.586.127Z"
|
||||
fill="#E27625"
|
||||
stroke="#E27625"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m16.428 21.738-2.237 3.427 7.97.368-.266-8.709-5.467 4.914ZM32.229 21.738l-5.552-5.012-.181 8.807 7.97-.368-2.237-3.427ZM16.861 37.428l4.824-2.365-4.152-3.285-.672 5.65ZM26.971 35.063l4.81 2.365-.657-5.65-4.153 3.285Z"
|
||||
fill="#E27625"
|
||||
stroke="#E27625"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m31.78 37.428-4.81-2.365.392 3.172-.042 1.345 4.46-2.152ZM16.861 37.428l4.475 2.152-.028-1.345.377-3.172-4.824 2.365Z"
|
||||
fill="#D5BFB2"
|
||||
stroke="#D5BFB2"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m21.42 29.682-4-1.19 2.825-1.316 1.174 2.506ZM27.236 29.682l1.175-2.506 2.838 1.317-4.013 1.19Z"
|
||||
fill="#233447"
|
||||
stroke="#233447"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m16.861 37.427.7-5.834-4.447.128 3.747 5.706ZM31.096 31.593l.685 5.834 3.761-5.706-4.446-.128ZM34.465 25.165l-7.97.368.741 4.15 1.175-2.507 2.838 1.317 3.216-3.328ZM17.42 28.493l2.825-1.317 1.175 2.506.74-4.149-7.97-.368 3.23 3.328Z"
|
||||
fill="#CC6228"
|
||||
stroke="#CC6228"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m14.19 25.165 3.343 6.613-.112-3.285-3.23-3.328ZM31.25 28.493l-.126 3.285 3.342-6.613-3.216 3.328ZM22.161 25.533l-.741 4.149.937 4.9.21-6.458-.406-2.591ZM26.495 25.533l-.391 2.577.196 6.471.937-4.9-.741-4.148Z"
|
||||
fill="#E27525"
|
||||
stroke="#E27525"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m27.237 29.682-.937 4.9.671.481 4.153-3.285.126-3.285-4.013 1.19ZM17.42 28.493l.112 3.285 4.153 3.285.671-.481-.937-4.9-3.999-1.19Z"
|
||||
fill="#F5841F"
|
||||
stroke="#F5841F"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m27.32 39.58.042-1.345-.363-.312h-5.342l-.35.312.029 1.345-4.475-2.152 1.566 1.303 3.175 2.223h5.439l3.188-2.224 1.552-1.302-4.46 2.152Z"
|
||||
fill="#C0AC9D"
|
||||
stroke="#C0AC9D"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m26.97 35.063-.67-.482h-3.944l-.67.482-.378 3.172.35-.312h5.34l.364.312-.391-3.172Z"
|
||||
fill="#161616"
|
||||
stroke="#161616"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m41.234 18.283 1.188-5.863-1.79-5.451-13.66 10.266 5.257 4.503 7.425 2.195 1.636-1.94-.713-.524 1.132-1.048-.867-.68 1.133-.878-.741-.58ZM6.234 12.42l1.203 5.863-.77.58 1.147.878-.867.68L8.08 21.47l-.713.524 1.636 1.94 7.425-2.195 5.257-4.503L8.025 6.97l-1.79 5.452Z"
|
||||
fill="#763E1A"
|
||||
stroke="#763E1A"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="m39.654 23.933-7.425-2.195 2.237 3.427-3.342 6.613 4.419-.057h6.6l-2.49-7.788ZM16.428 21.738l-7.425 2.195-2.475 7.788h6.586l4.418.056-3.342-6.612 2.238-3.427ZM26.495 25.533l.476-8.298 2.153-5.905h-9.592l2.153 5.905.476 8.298.181 2.605.014 6.443H26.3l.014-6.443.182-2.605Z"
|
||||
fill="#F5841F"
|
||||
stroke="#F5841F"
|
||||
strokeWidth="0.223"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CoinbaseWalletIcon = ({ width, height }: IconProps) => (
|
||||
<svg
|
||||
width={width}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Ledger entries
|
||||
|
||||
## Ledger entries tab in portfolio
|
||||
|
||||
When I enter on ledger entries tab in portfolio page
|
||||
|
||||
- **Must** see a form for download report file (<a name="7007-LEEN-001" href="#7007-LEEN-001">7007-LEEN-001</a>)
|
||||
- in the form **Must** see a dropdown for select an asset, in which reports will be downloaded (<a name="7007-LEEN-002" href="#7007-LEEN-002">7007-LEEN-002</a>)
|
||||
- in the form **Must** see inputs for select time period, in which reports will be downloaded (<a name="7007-LEEN-003" href="#7007-LEEN-003">7007-LEEN-003</a>)
|
||||
- default preselected period **Must** be the last 7 days (<a name="7007-LEEN-004" href="#7007-LEEN-004">7007-LEEN-004</a>)
|
||||
- during download a loader component **Must** be visible and all interactive elements in the form **Must** be disabled (<a name="7007-LEEN-005" href="#7007-LEEN-005">7007-LEEN-005</a>)
|
||||
Reference in New Issue
Block a user