From 8f8a727b4dfbe65e226548d7bd590b0be3a20967 Mon Sep 17 00:00:00 2001 From: Edd Date: Fri, 18 Nov 2022 17:10:57 +0000 Subject: [PATCH] feat(explorer): add specific link types, start specific tx views (#2152) * feat(explorer): add specific link types, start specific tx views * feat(explorer): add raw tx view * chore(explorer): remove default nx vega url * fix(explorer): improve prop typing for link components * fix(explorer): remove console log * fix(explorer): remove unused import * fix(explorer): refactor txdetails component selection --- apps/explorer/.env.capsule | 1 - apps/explorer/.env.devnet | 1 - apps/explorer/.env.mainnet | 1 - apps/explorer/.env.sandbox | 1 - apps/explorer/.env.stagnet1 | 1 - apps/explorer/.env.stagnet3 | 1 - apps/explorer/.env.testnet | 7 +- apps/explorer/.env.vegacapsule | 1 - apps/explorer/README.md | 31 +++--- .../components/links/asset-link/Asset.graphql | 8 ++ .../links/asset-link/__generated__/Asset.ts | 51 +++++++++ .../links/asset-link/asset-link.spec.tsx | 63 +++++++++++ .../links/asset-link/asset-link.tsx | 35 ++++++ .../links/block-link/block-link.tsx | 19 ++++ .../src/app/components/links/index.ts | 5 + .../links/market-link/Market.graphql | 11 ++ .../links/market-link/__generated__/Market.ts | 54 ++++++++++ .../links/market-link/market-link.spec.tsx | 66 ++++++++++++ .../links/market-link/market-link.tsx | 35 ++++++ .../components/links/node-link/Node.graphql | 7 ++ .../links/node-link/__generated__/Node.ts | 50 +++++++++ .../links/node-link/node-link.spec.tsx | 62 +++++++++++ .../components/links/node-link/node-link.tsx | 30 ++++++ .../links/party-link/party-link.tsx | 19 ++++ .../nested-data-list.spec.tsx | 4 +- .../nested-data-list/nested-data-list.tsx | 2 +- .../txs/details/shared/tx-details-shared.tsx | 71 ++++++++++++ .../app/components/txs/details/tx-batch.tsx | 75 +++++++++++++ .../components/txs/details/tx-chain-event.tsx | 64 +++++++++++ .../txs/details/tx-details-wrapper.tsx | 83 ++++++++++++++ .../app/components/txs/details/tx-generic.tsx | 32 ++++++ .../components/txs/details/tx-hearbeat.tsx | 86 +++++++++++++++ .../components/txs/details/tx-lp-amend.tsx | 45 ++++++++ .../app/components/txs/details/tx-order.tsx | 46 ++++++++ .../src/app/routes/blocks/id/block.spec.tsx | 21 ++-- .../src/app/routes/blocks/id/block.tsx | 13 ++- .../parties/id/__generated__/party-assets.ts | 5 +- .../src/app/routes/parties/id/index.tsx | 6 +- .../routes/parties/id/party-assets.graphql | 3 + apps/explorer/src/app/routes/txs/id/index.tsx | 19 ++-- .../src/app/routes/txs/id/tx-content.tsx | 3 +- .../src/app/routes/txs/id/tx-details.spec.tsx | 19 ++-- .../src/app/routes/txs/id/tx-details.tsx | 15 +-- .../routes/types/block-explorer-response.d.ts | 102 +++++++++++++++++- .../routes/validators/__generated__/nodes.ts | 5 +- .../src/app/routes/validators/nodes.graphql | 3 +- 46 files changed, 1197 insertions(+), 85 deletions(-) create mode 100644 apps/explorer/src/app/components/links/asset-link/Asset.graphql create mode 100644 apps/explorer/src/app/components/links/asset-link/__generated__/Asset.ts create mode 100644 apps/explorer/src/app/components/links/asset-link/asset-link.spec.tsx create mode 100644 apps/explorer/src/app/components/links/asset-link/asset-link.tsx create mode 100644 apps/explorer/src/app/components/links/block-link/block-link.tsx create mode 100644 apps/explorer/src/app/components/links/index.ts create mode 100644 apps/explorer/src/app/components/links/market-link/Market.graphql create mode 100644 apps/explorer/src/app/components/links/market-link/__generated__/Market.ts create mode 100644 apps/explorer/src/app/components/links/market-link/market-link.spec.tsx create mode 100644 apps/explorer/src/app/components/links/market-link/market-link.tsx create mode 100644 apps/explorer/src/app/components/links/node-link/Node.graphql create mode 100644 apps/explorer/src/app/components/links/node-link/__generated__/Node.ts create mode 100644 apps/explorer/src/app/components/links/node-link/node-link.spec.tsx create mode 100644 apps/explorer/src/app/components/links/node-link/node-link.tsx create mode 100644 apps/explorer/src/app/components/links/party-link/party-link.tsx create mode 100644 apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-batch.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-chain-event.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-generic.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-hearbeat.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-lp-amend.tsx create mode 100644 apps/explorer/src/app/components/txs/details/tx-order.tsx diff --git a/apps/explorer/.env.capsule b/apps/explorer/.env.capsule index 828306763..671cb92cc 100644 --- a/apps/explorer/.env.capsule +++ b/apps/explorer/.env.capsule @@ -2,7 +2,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api NX_TENDERMINT_URL=http://localhost:26617 NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket -NX_VEGA_URL=http://localhost:3028/query NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"} NX_VEGA_ENV=CUSTOM NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions diff --git a/apps/explorer/.env.devnet b/apps/explorer/.env.devnet index 76c07de0c..551fcfdaf 100644 --- a/apps/explorer/.env.devnet +++ b/apps/explorer/.env.devnet @@ -3,7 +3,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-exp NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json -NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"} NX_VEGA_ENV=DEVNET NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions diff --git a/apps/explorer/.env.mainnet b/apps/explorer/.env.mainnet index 91e818a65..5440658ed 100644 --- a/apps/explorer/.env.mainnet +++ b/apps/explorer/.env.mainnet @@ -3,7 +3,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-exp NX_TENDERMINT_URL=https://be.explorer.vega.xyz NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json -NX_VEGA_URL=https://api.vega.xyz/query NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"} NX_VEGA_ENV=MAINNET NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions diff --git a/apps/explorer/.env.sandbox b/apps/explorer/.env.sandbox index 57b64c79d..361575744 100644 --- a/apps/explorer/.env.sandbox +++ b/apps/explorer/.env.sandbox @@ -1,5 +1,4 @@ # App configuration variables -NX_VEGA_URL=https://api.sandbox.vega.xyz/graphql NX_VEGA_ENV=SANDBOX NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz diff --git a/apps/explorer/.env.stagnet1 b/apps/explorer/.env.stagnet1 index 9051f3a38..be6b48499 100644 --- a/apps/explorer/.env.stagnet1 +++ b/apps/explorer/.env.stagnet1 @@ -6,7 +6,6 @@ NX_VEGA_ENV=STAGNET1 NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"} NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz -NX_VEGA_URL=https://api.n00.stagnet1.vega.xyz/graphql NX_VEGA_WALLET_URL=http://localhost:1789 NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket diff --git a/apps/explorer/.env.stagnet3 b/apps/explorer/.env.stagnet3 index b8d197cdd..3caa7d700 100644 --- a/apps/explorer/.env.stagnet3 +++ b/apps/explorer/.env.stagnet3 @@ -3,7 +3,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-exp NX_TENDERMINT_URL=https://n01.stagnet3.vega.xyz/tm NX_TENDERMINT_WEBSOCKET_URL=wss://n01.stagnet3.vega.xyz/tm/websocket NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json -NX_VEGA_URL=https://api.n01.stagnet3.vega.xyz/graphql NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"} NX_VEGA_ENV=STAGNET3 NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions diff --git a/apps/explorer/.env.testnet b/apps/explorer/.env.testnet index 12236dbe5..6afef2cb8 100644 --- a/apps/explorer/.env.testnet +++ b/apps/explorer/.env.testnet @@ -1,11 +1,12 @@ # App configuration variables NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api -NX_TENDERMINT_URL=https://be.testnet.vega.xyz +NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest -NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/ +NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.testnet.vega.xyz/ NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"} NX_VEGA_ENV=TESTNET NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions -NX_VEGA_URL=https://api.n09.testnet.vega.xyz/graphql +NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz +NX_VEGA_NETWORKS={} \ No newline at end of file diff --git a/apps/explorer/.env.vegacapsule b/apps/explorer/.env.vegacapsule index a0b854c9a..ebc25363c 100644 --- a/apps/explorer/.env.vegacapsule +++ b/apps/explorer/.env.vegacapsule @@ -2,7 +2,6 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api NX_TENDERMINT_URL=http://localhost:26607/ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket -NX_VEGA_URL=http://localhost:3003/query NX_VEGA_ENV=CUSTOM NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_BLOCK_EXPLORER= diff --git a/apps/explorer/README.md b/apps/explorer/README.md index d4d053613..ebe40b16e 100644 --- a/apps/explorer/README.md +++ b/apps/explorer/README.md @@ -45,22 +45,21 @@ yarn nx run explorer:serve --env={env} # e.g. stagnet3 There are a few different configuration options offered for this app: -| **Flag** | **Purpose** | -| -------------------------------- | ---------------------------------------------------------------------------------------------------- | --- | -| `NX_CHAIN_EXPLORER_URL` | The URL of the chain explorer service for decoding transactions | -| `NX_TENDERMINT_URL` | The Tendermint REST URL for the Vega consesus engine | -| `NX_TENDERMINT_WEBSOCKET_URL` | The Tendermint Websocket URL for the Vega consensus engine | -| `NX_VEGA_URL` | The GraphQl query endpoint of a [Vega data node](https://github.com/vegaprotocol/networks#data-node) | -| `NX_VEGA_ENV` | The name of the currently connected vega environment | | -| `NX_EXPLORER_ASSETS` | Enable the assets page for the explorer | -| `NX_EXPLORER_GENESIS` | Enable the genesis page for the explorer | -| `NX_EXPLORER_GOVERNANCE` | Enable the governance page for the explorer | -| `NX_EXPLORER_MARKETS` | Enable the markets page for the explorer | -| `NX_EXPLORER_ORACLES` | Enable the oracles page for the explorer | -| `NX_EXPLORER_TXS_LIST` | Enable the transactions list page for the explorer | -| `NX_EXPLORER_NETWORK_PARAMETERS` | Enable the network parameters page for the explorer | -| `NX_EXPLORER_PARTIES` | Enable the parties page for the explorer | -| `NX_EXPLORER_VALIDATORS` | Enable the validators page for the explorer | +| **Flag** | **Purpose** | +| -------------------------------- | --------------------------------------------------------------- | --- | +| `NX_CHAIN_EXPLORER_URL` | The URL of the chain explorer service for decoding transactions | +| `NX_TENDERMINT_URL` | The Tendermint REST URL for the Vega consesus engine | +| `NX_TENDERMINT_WEBSOCKET_URL` | The Tendermint Websocket URL for the Vega consensus engine | +| `NX_VEGA_ENV` | The name of the currently connected vega environment | | +| `NX_EXPLORER_ASSETS` | Enable the assets page for the explorer | +| `NX_EXPLORER_GENESIS` | Enable the genesis page for the explorer | +| `NX_EXPLORER_GOVERNANCE` | Enable the governance page for the explorer | +| `NX_EXPLORER_MARKETS` | Enable the markets page for the explorer | +| `NX_EXPLORER_ORACLES` | Enable the oracles page for the explorer | +| `NX_EXPLORER_TXS_LIST` | Enable the transactions list page for the explorer | +| `NX_EXPLORER_NETWORK_PARAMETERS` | Enable the network parameters page for the explorer | +| `NX_EXPLORER_PARTIES` | Enable the parties page for the explorer | +| `NX_EXPLORER_VALIDATORS` | Enable the validators page for the explorer | ## Testing diff --git a/apps/explorer/src/app/components/links/asset-link/Asset.graphql b/apps/explorer/src/app/components/links/asset-link/Asset.graphql new file mode 100644 index 000000000..dbbd0b138 --- /dev/null +++ b/apps/explorer/src/app/components/links/asset-link/Asset.graphql @@ -0,0 +1,8 @@ +query ExplorerAsset($id: ID!) { + asset(id: $id) { + id + name + status + decimals + } +} diff --git a/apps/explorer/src/app/components/links/asset-link/__generated__/Asset.ts b/apps/explorer/src/app/components/links/asset-link/__generated__/Asset.ts new file mode 100644 index 000000000..3101fe84a --- /dev/null +++ b/apps/explorer/src/app/components/links/asset-link/__generated__/Asset.ts @@ -0,0 +1,51 @@ +import { Schema as Types } from '@vegaprotocol/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type ExplorerAssetQueryVariables = Types.Exact<{ + id: Types.Scalars['ID']; +}>; + + +export type ExplorerAssetQuery = { __typename?: 'Query', asset?: { __typename?: 'Asset', id: string, name: string, status: Types.AssetStatus, decimals: number } | null }; + + +export const ExplorerAssetDocument = gql` + query ExplorerAsset($id: ID!) { + asset(id: $id) { + id + name + status + decimals + } +} + `; + +/** + * __useExplorerAssetQuery__ + * + * To run a query within a React component, call `useExplorerAssetQuery` and pass it any options that fit your needs. + * When your component renders, `useExplorerAssetQuery` 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 } = useExplorerAssetQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useExplorerAssetQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ExplorerAssetDocument, options); + } +export function useExplorerAssetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ExplorerAssetDocument, options); + } +export type ExplorerAssetQueryHookResult = ReturnType; +export type ExplorerAssetLazyQueryHookResult = ReturnType; +export type ExplorerAssetQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/explorer/src/app/components/links/asset-link/asset-link.spec.tsx b/apps/explorer/src/app/components/links/asset-link/asset-link.spec.tsx new file mode 100644 index 000000000..4c898ca5b --- /dev/null +++ b/apps/explorer/src/app/components/links/asset-link/asset-link.spec.tsx @@ -0,0 +1,63 @@ +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import type { MockedResponse } from '@apollo/client/testing'; +import { render } from '@testing-library/react'; +import AssetLink from './asset-link'; +import { ExplorerAssetDocument } from './__generated__/Asset'; + +function renderComponent(id: string, mock: MockedResponse[]) { + return ( + + + + + + ); +} + +describe('Asset link component', () => { + it('Renders the ID at first', () => { + const res = render(renderComponent('123', [])); + expect(res.getByText('123')).toBeInTheDocument(); + }); + + it('Renders the asset name when the query returns a result', async () => { + const mock = { + request: { + query: ExplorerAssetDocument, + variables: { + id: '123', + }, + }, + result: { + data: { + asset: { + id: '123', + name: 'test-label', + status: 'irrelevant-test-data', + decimals: 18, + }, + }, + }, + }; + + const res = render(renderComponent('123', [mock])); + expect(res.getByText('123')).toBeInTheDocument(); + expect(await res.findByText('test-label')).toBeInTheDocument(); + }); + + it('Leaves the asset id when the asset is not found', async () => { + const mock = { + request: { + query: ExplorerAssetDocument, + variables: { + id: '123', + }, + }, + error: new Error('No such asset'), + }; + + const res = render(renderComponent('123', [mock])); + expect(await res.findByText('123')).toBeInTheDocument(); + }); +}); diff --git a/apps/explorer/src/app/components/links/asset-link/asset-link.tsx b/apps/explorer/src/app/components/links/asset-link/asset-link.tsx new file mode 100644 index 000000000..65b0e2229 --- /dev/null +++ b/apps/explorer/src/app/components/links/asset-link/asset-link.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Routes } from '../../../routes/route-names'; +import { useExplorerAssetQuery } from './__generated__/Asset'; +import { Link } from 'react-router-dom'; + +import type { ComponentProps } from 'react'; + +export type AssetLinkProps = Partial> & { + id: string; +}; + +/** + * Given an asset ID, it will fetch the asset name and show that, + * with a link to the assets list. If the name does not come back + * it will use the ID instead. + */ +const AssetLink = ({ id, ...props }: AssetLinkProps) => { + const { data } = useExplorerAssetQuery({ + variables: { id }, + }); + + let label: string = id; + + if (data?.asset?.name) { + label = data.asset.name; + } + + return ( + + {label} + + ); +}; + +export default AssetLink; diff --git a/apps/explorer/src/app/components/links/block-link/block-link.tsx b/apps/explorer/src/app/components/links/block-link/block-link.tsx new file mode 100644 index 000000000..8707ec997 --- /dev/null +++ b/apps/explorer/src/app/components/links/block-link/block-link.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Routes } from '../../../routes/route-names'; +import { Link } from 'react-router-dom'; + +import type { ComponentProps } from 'react'; + +export type BlockLinkProps = Partial> & { + height: string; +}; + +const BlockLink = ({ height, ...props }: BlockLinkProps) => { + return ( + + {height} + + ); +}; + +export default BlockLink; diff --git a/apps/explorer/src/app/components/links/index.ts b/apps/explorer/src/app/components/links/index.ts new file mode 100644 index 000000000..b3d5fb0f2 --- /dev/null +++ b/apps/explorer/src/app/components/links/index.ts @@ -0,0 +1,5 @@ +export { default as BlockLink } from './block-link/block-link'; +export { default as PartyLink } from './party-link/party-link'; +export { default as NodeLink } from './node-link/node-link'; +export { default as MarketLink } from './market-link/market-link'; +export { default as AssetLink } from './asset-link/asset-link'; diff --git a/apps/explorer/src/app/components/links/market-link/Market.graphql b/apps/explorer/src/app/components/links/market-link/Market.graphql new file mode 100644 index 000000000..57d775e04 --- /dev/null +++ b/apps/explorer/src/app/components/links/market-link/Market.graphql @@ -0,0 +1,11 @@ +query ExplorerMarket($id: ID!) { + market(id: $id) { + id + tradableInstrument { + instrument { + name + } + } + state + } +} diff --git a/apps/explorer/src/app/components/links/market-link/__generated__/Market.ts b/apps/explorer/src/app/components/links/market-link/__generated__/Market.ts new file mode 100644 index 000000000..169e31154 --- /dev/null +++ b/apps/explorer/src/app/components/links/market-link/__generated__/Market.ts @@ -0,0 +1,54 @@ +import { Schema as Types } from '@vegaprotocol/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type ExplorerMarketQueryVariables = Types.Exact<{ + id: Types.Scalars['ID']; +}>; + + +export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } | null }; + + +export const ExplorerMarketDocument = gql` + query ExplorerMarket($id: ID!) { + market(id: $id) { + id + tradableInstrument { + instrument { + name + } + } + state + } +} + `; + +/** + * __useExplorerMarketQuery__ + * + * To run a query within a React component, call `useExplorerMarketQuery` and pass it any options that fit your needs. + * When your component renders, `useExplorerMarketQuery` 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 } = useExplorerMarketQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useExplorerMarketQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ExplorerMarketDocument, options); + } +export function useExplorerMarketLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ExplorerMarketDocument, options); + } +export type ExplorerMarketQueryHookResult = ReturnType; +export type ExplorerMarketLazyQueryHookResult = ReturnType; +export type ExplorerMarketQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/explorer/src/app/components/links/market-link/market-link.spec.tsx b/apps/explorer/src/app/components/links/market-link/market-link.spec.tsx new file mode 100644 index 000000000..c416f7c04 --- /dev/null +++ b/apps/explorer/src/app/components/links/market-link/market-link.spec.tsx @@ -0,0 +1,66 @@ +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import type { MockedResponse } from '@apollo/client/testing'; +import { render } from '@testing-library/react'; +import MarketLink from './market-link'; +import { ExplorerMarketDocument } from './__generated__/Market'; + +function renderComponent(id: string, mock: MockedResponse[]) { + return ( + + + + + + ); +} + +describe('Market link component', () => { + it('Renders the ID at first', () => { + const res = render(renderComponent('123', [])); + expect(res.getByText('123')).toBeInTheDocument(); + }); + + it('Renders the market name when the query returns a result', async () => { + const mock = { + request: { + query: ExplorerMarketDocument, + variables: { + id: '123', + }, + }, + result: { + data: { + market: { + id: '123', + state: 'irrelevant-test-data', + tradableInstrument: { + instrument: { + name: 'test-label', + }, + }, + }, + }, + }, + }; + + const res = render(renderComponent('123', [mock])); + expect(res.getByText('123')).toBeInTheDocument(); + expect(await res.findByText('test-label')).toBeInTheDocument(); + }); + + it('Leaves the market id when the market is not found', async () => { + const mock = { + request: { + query: ExplorerMarketDocument, + variables: { + id: '123', + }, + }, + error: new Error('No such market'), + }; + + const res = render(renderComponent('123', [mock])); + expect(await res.findByText('123')).toBeInTheDocument(); + }); +}); diff --git a/apps/explorer/src/app/components/links/market-link/market-link.tsx b/apps/explorer/src/app/components/links/market-link/market-link.tsx new file mode 100644 index 000000000..e5789a6ca --- /dev/null +++ b/apps/explorer/src/app/components/links/market-link/market-link.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Routes } from '../../../routes/route-names'; +import { useExplorerMarketQuery } from './__generated__/Market'; +import { Link } from 'react-router-dom'; + +import type { ComponentProps } from 'react'; + +export type MarketLinkProps = Partial> & { + id: string; +}; + +/** + * Given a market ID, it will fetch the market name and show that, + * with a link to the markets list. If the name does not come back + * it will use the ID instead + */ +const MarketLink = ({ id, ...props }: MarketLinkProps) => { + const { data } = useExplorerMarketQuery({ + variables: { id }, + }); + + let label: string = id; + + if (data?.market?.tradableInstrument.instrument.name) { + label = data.market.tradableInstrument.instrument.name; + } + + return ( + + {label} + + ); +}; + +export default MarketLink; diff --git a/apps/explorer/src/app/components/links/node-link/Node.graphql b/apps/explorer/src/app/components/links/node-link/Node.graphql new file mode 100644 index 000000000..566724689 --- /dev/null +++ b/apps/explorer/src/app/components/links/node-link/Node.graphql @@ -0,0 +1,7 @@ +query ExplorerNode($id: ID!) { + node(id: $id) { + id + name + status + } +} diff --git a/apps/explorer/src/app/components/links/node-link/__generated__/Node.ts b/apps/explorer/src/app/components/links/node-link/__generated__/Node.ts new file mode 100644 index 000000000..bbc027393 --- /dev/null +++ b/apps/explorer/src/app/components/links/node-link/__generated__/Node.ts @@ -0,0 +1,50 @@ +import { Schema as Types } from '@vegaprotocol/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type ExplorerNodeQueryVariables = Types.Exact<{ + id: Types.Scalars['ID']; +}>; + + +export type ExplorerNodeQuery = { __typename?: 'Query', node?: { __typename?: 'Node', id: string, name: string, status: Types.NodeStatus } | null }; + + +export const ExplorerNodeDocument = gql` + query ExplorerNode($id: ID!) { + node(id: $id) { + id + name + status + } +} + `; + +/** + * __useExplorerNodeQuery__ + * + * To run a query within a React component, call `useExplorerNodeQuery` and pass it any options that fit your needs. + * When your component renders, `useExplorerNodeQuery` 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 } = useExplorerNodeQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useExplorerNodeQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ExplorerNodeDocument, options); + } +export function useExplorerNodeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ExplorerNodeDocument, options); + } +export type ExplorerNodeQueryHookResult = ReturnType; +export type ExplorerNodeLazyQueryHookResult = ReturnType; +export type ExplorerNodeQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/explorer/src/app/components/links/node-link/node-link.spec.tsx b/apps/explorer/src/app/components/links/node-link/node-link.spec.tsx new file mode 100644 index 000000000..86de55a5d --- /dev/null +++ b/apps/explorer/src/app/components/links/node-link/node-link.spec.tsx @@ -0,0 +1,62 @@ +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import type { MockedResponse } from '@apollo/client/testing'; +import { render } from '@testing-library/react'; +import NodeLink from './node-link'; +import { ExplorerNodeDocument } from './__generated__/Node'; + +function renderComponent(id: string, mock: MockedResponse[]) { + return ( + + + + + + ); +} + +describe('Node link component', () => { + it('Renders the ID at first', () => { + const res = render(renderComponent('123', [])); + expect(res.getByText('123')).toBeInTheDocument(); + }); + + it('Renders the node name when the query returns a result', async () => { + const mock = { + request: { + query: ExplorerNodeDocument, + variables: { + id: '123', + }, + }, + result: { + data: { + node: { + id: '123', + status: 'irrelevant-test-data', + name: 'test-label', + }, + }, + }, + }; + + const res = render(renderComponent('123', [mock])); + expect(res.getByText('123')).toBeInTheDocument(); + expect(await res.findByText('test-label')).toBeInTheDocument(); + }); + + it('Leaves the node id when the node is not found', async () => { + const mock = { + request: { + query: ExplorerNodeDocument, + variables: { + id: '123', + }, + }, + error: new Error('No such node'), + }; + + const res = render(renderComponent('123', [mock])); + expect(await res.findByText('123')).toBeInTheDocument(); + }); +}); diff --git a/apps/explorer/src/app/components/links/node-link/node-link.tsx b/apps/explorer/src/app/components/links/node-link/node-link.tsx new file mode 100644 index 000000000..3759e96e3 --- /dev/null +++ b/apps/explorer/src/app/components/links/node-link/node-link.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Routes } from '../../../routes/route-names'; +import { useExplorerNodeQuery } from './__generated__/Node'; +import { Link } from 'react-router-dom'; + +import type { ComponentProps } from 'react'; + +export type NodeLinkProps = Partial> & { + id: string; +}; + +const NodeLink = ({ id, ...props }: NodeLinkProps) => { + const { data } = useExplorerNodeQuery({ + variables: { id }, + }); + + let label: string = id; + + if (data?.node?.name) { + label = data.node.name; + } + + return ( + + {label} + + ); +}; + +export default NodeLink; diff --git a/apps/explorer/src/app/components/links/party-link/party-link.tsx b/apps/explorer/src/app/components/links/party-link/party-link.tsx new file mode 100644 index 000000000..d07e93c64 --- /dev/null +++ b/apps/explorer/src/app/components/links/party-link/party-link.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Routes } from '../../../routes/route-names'; +import { Link } from 'react-router-dom'; + +import type { ComponentProps } from 'react'; + +export type PartyLinkProps = Partial> & { + id: string; +}; + +const PartyLink = ({ id, ...props }: PartyLinkProps) => { + return ( + + {id} + + ); +}; + +export default PartyLink; diff --git a/apps/explorer/src/app/components/nested-data-list/nested-data-list.spec.tsx b/apps/explorer/src/app/components/nested-data-list/nested-data-list.spec.tsx index ccdb41af1..039350bfa 100644 --- a/apps/explorer/src/app/components/nested-data-list/nested-data-list.spec.tsx +++ b/apps/explorer/src/app/components/nested-data-list/nested-data-list.spec.tsx @@ -41,10 +41,10 @@ describe('NestedDataList', () => { const parent = getAllByRole('listitem', { name: 'Validator Heartbeat' }); const nestedContainer = parent[0].querySelector('[aria-hidden]'); const expandBtn = parent[0].querySelector('button'); - expect(nestedContainer).toHaveAttribute('aria-hidden', 'true'); + expect(nestedContainer).toHaveAttribute('aria-hidden', 'false'); await user.click(expandBtn as HTMLButtonElement); await waitFor(() => nestedContainer); - expect(nestedContainer).toHaveAttribute('aria-hidden', 'false'); + expect(nestedContainer).toHaveAttribute('aria-hidden', 'true'); }); it('add border to the title of the parent', () => { diff --git a/apps/explorer/src/app/components/nested-data-list/nested-data-list.tsx b/apps/explorer/src/app/components/nested-data-list/nested-data-list.tsx index a302721f3..de6d59517 100644 --- a/apps/explorer/src/app/components/nested-data-list/nested-data-list.tsx +++ b/apps/explorer/src/app/components/nested-data-list/nested-data-list.tsx @@ -55,7 +55,7 @@ const NestedDataListItem = ({ value, index, }: NestedDataListItemProps) => { - const [isCollapsed, setCollapsed] = useState(true); + const [isCollapsed, setCollapsed] = useState(false); const toggleVisible = useCallback( (event: React.MouseEvent) => { event.stopPropagation(); diff --git a/apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx b/apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx new file mode 100644 index 000000000..604667de7 --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/shared/tx-details-shared.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import { TableRow, TableCell } from '../../../table'; +import { BlockLink, PartyLink } from '../../../links/'; +import { TimeAgo } from '../../../time-ago'; + +import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response'; +import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response'; + +interface TxDetailsSharedProps { + txData: BlockExplorerTransactionResult | undefined; + pubKey: string | undefined; + blockData: TendermintBlocksResponse | undefined; +} + +/** + * 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 + * change as the views get more bespoke. + */ +export const TxDetailsShared = ({ + txData, + pubKey, + blockData, +}: TxDetailsSharedProps) => { + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + + const time: string = blockData?.result.block.header.time || ''; + const height: string = blockData?.result.block.header.height || ''; + let timeFormatted = ''; + + if (time) { + timeFormatted = new Date(time).toLocaleString(); + } + + return ( + <> + + {t('Hash')} + {txData.hash} + + + {t('Submitter')} + {pubKey ? : '-'} + + + {t('Block')} + + + + + + {t('Time')} + + {time ? ( +
+ {timeFormatted} + + + +
+ ) : ( + '-' + )} +
+
+ + ); +}; diff --git a/apps/explorer/src/app/components/txs/details/tx-batch.tsx b/apps/explorer/src/app/components/txs/details/tx-batch.tsx new file mode 100644 index 000000000..01008989b --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-batch.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import type { + BlockExplorerTransactionResult, + BatchMarketInstructions, +} from '../../../routes/types/block-explorer-response'; +import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response'; +import { TxDetailsShared } from './shared/tx-details-shared'; +import { TableWithTbody, TableRow, TableCell } from '../../table'; + +interface TxDetailsBatchProps { + txData: BlockExplorerTransactionResult | undefined; + pubKey: string | undefined; + blockData: TendermintBlocksResponse | undefined; +} + +/** + * BatchMarketInstructions are sets of changes in three categories: new orders (submissions), + * order change s(amendments) and cancellations. + * + * Design considerations for batch: + * - So far it's very basic details about the size of the batch + */ +export const TxDetailsBatch = ({ + txData, + pubKey, + blockData, +}: TxDetailsBatchProps) => { + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + + const cmd = txData.command as BatchMarketInstructions; + + const batchSubmissions = cmd.batchMarketInstructions.submissions.length; + const batchAmendments = cmd.batchMarketInstructions.amendments.length; + const batchCancellations = cmd.batchMarketInstructions.cancellations.length; + const batchTotal = batchSubmissions + batchAmendments + batchCancellations; + + return ( + + + + {t('Batch size')} + + {batchTotal} + + + + + {t('Submissions')} + + + {batchSubmissions} + + + + + {t('Amendments')} + + + {batchAmendments} + + + + + {t('Cancellations')} + + + {batchCancellations} + + + + ); +}; diff --git a/apps/explorer/src/app/components/txs/details/tx-chain-event.tsx b/apps/explorer/src/app/components/txs/details/tx-chain-event.tsx new file mode 100644 index 000000000..66d1cb09e --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-chain-event.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import type { + BlockExplorerTransactionResult, + ChainEvent, +} from '../../../routes/types/block-explorer-response'; +import { AssetLink, PartyLink } from '../../links'; +import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response'; +import { TxDetailsShared } from './shared/tx-details-shared'; +import { TableCell, TableRow, TableWithTbody } from '../../table'; + +interface TxDetailsChainEventProps { + txData: BlockExplorerTransactionResult | undefined; + pubKey: string | undefined; + blockData: TendermintBlocksResponse | undefined; +} + +/** + * Chain events are external blockchain events (e.g. Ethereum) reported by bridge + * Multiple events will relay the same data, from each validator, so that the + * deposit/withdrawal can be verified independently. + * + * Design considerations so far: + * - The ethereum address should be a link to an Ethereum explorer + * - Sender and recipient are shown because they are easy + * - Amount is not shown because there is no formatter by asset component + */ +export const TxDetailsChainEvent = ({ + txData, + pubKey, + blockData, +}: TxDetailsChainEventProps) => { + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + const cmd = txData.command as ChainEvent; + const assetId = cmd.chainEvent.erc20.deposit.vegaAssetId; + const sender = cmd.chainEvent.erc20.deposit.sourceEthereumAddress; + const recipient = cmd.chainEvent.erc20.deposit.targetPartyId; + + return ( + + + + {t('Asset')} + + + + + + {t('Sender')} + + {sender} + + + + {t('Recipient')} + + + + + + ); +}; diff --git a/apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx b/apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx new file mode 100644 index 000000000..b6daec5fc --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-details-wrapper.tsx @@ -0,0 +1,83 @@ +import React, { useMemo } from 'react'; +import { DATA_SOURCES } from '../../../config'; +import { t, useFetch } from '@vegaprotocol/react-helpers'; +import { TxDetailsOrder } from './tx-order'; +import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response'; +import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response'; +import { TxDetailsHeartbeat } from './tx-hearbeat'; +import { TxDetailsLPAmend } from './tx-lp-amend'; +import { TxDetailsGeneric } from './tx-generic'; +import { TxDetailsBatch } from './tx-batch'; +import { TxDetailsChainEvent } from './tx-chain-event'; +import { TxContent } from '../../../routes/txs/id/tx-content'; + +type resultOrNull = BlockExplorerTransactionResult | undefined; + +interface TxDetailsWrapperProps { + txData: resultOrNull; + pubKey: string | undefined; + height: string; +} + +export const TxDetailsWrapper = ({ + txData, + pubKey, + height, +}: TxDetailsWrapperProps) => { + const { + state: { data: blockData }, + } = useFetch( + `${DATA_SOURCES.tendermintUrl}/block?height=${height}` + ); + + const child = useMemo(() => getTransactionComponent(txData), [txData]); + + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + + return ( + <> +
{child({ txData, pubKey, blockData })}
+ +
+ {t('Decoded transaction')} + +
+ +
+ {t('Raw transaction')} + + {blockData?.result.block.data.txs[txData.index]} + +
+ + ); +}; + +/** + * Chooses the appropriate component to render the full details of a transaction + * + * @param txData + * @returns JSX.Element + */ +function getTransactionComponent(txData: resultOrNull) { + if (!txData) { + return TxDetailsGeneric; + } + + switch (txData.type) { + case 'Submit Order': + return TxDetailsOrder; + case 'Validator Heartbeat': + return TxDetailsHeartbeat; + case 'Amend LiquidityProvision Order': + return TxDetailsLPAmend; + case 'Batch Market Instructions': + return TxDetailsBatch; + case 'Chain Event': + return TxDetailsChainEvent; + default: + return TxDetailsGeneric; + } +} diff --git a/apps/explorer/src/app/components/txs/details/tx-generic.tsx b/apps/explorer/src/app/components/txs/details/tx-generic.tsx new file mode 100644 index 000000000..eabd333cb --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-generic.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response'; +import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response'; +import { TxDetailsShared } from './shared/tx-details-shared'; +import { TableWithTbody } from '../../table'; + +interface TxDetailsGenericProps { + txData: BlockExplorerTransactionResult | undefined; + pubKey: string | undefined; + blockData: TendermintBlocksResponse | undefined; +} + +/** + * If there is not yet a custom component for a transaction, just display + * the basic details. This allows someone to view the decoded transaction. + */ +export const TxDetailsGeneric = ({ + txData, + pubKey, + blockData, +}: TxDetailsGenericProps) => { + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + + return ( + + + + ); +}; diff --git a/apps/explorer/src/app/components/txs/details/tx-hearbeat.tsx b/apps/explorer/src/app/components/txs/details/tx-hearbeat.tsx new file mode 100644 index 000000000..7db5f9147 --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-hearbeat.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import type { + BlockExplorerTransactionResult, + ValidatorHeartbeat, +} from '../../../routes/types/block-explorer-response'; +import { BlockLink, NodeLink } from '../../links/'; +import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response'; +import { TxDetailsShared } from './shared/tx-details-shared'; +import { TableCell, TableRow, TableWithTbody } from '../../table'; + +/** + * Returns an integer representing how fresh the signature is, ranging from 1 to 500. + * Below 1 should be impossible - you can't sign a block before it is finished + * Any result about 500 is counted as stale in core and would be bad + * + * The precise freshness isn't that important, as long as it is within bounds + * + * @param txHeight string Block number that this signature was in + * @param signatureForHeight string Block number that this signature is signing + * @returns + */ +export function scoreFreshness( + txHeight: string, + signatureForHeight: string +): number { + const txHeightInt = parseInt(txHeight, 10); + const signatureForHeightInt = parseInt(signatureForHeight, 10); + + return txHeightInt - signatureForHeightInt; +} + +interface TxDetailsHeartbeatProps { + txData: BlockExplorerTransactionResult | undefined; + pubKey: string | undefined; + blockData: TendermintBlocksResponse | undefined; +} + +/** + * Validator Heartbeat transactions are a way for non-consensus validators to signal that they + * are still alive, still following along, and still valid for consideration in the validator set. + * To indicate they are still alive, they use their Ethereum and Vega private keys to provide two + * signatures of a recent block on chain. + * + * Blocks must be signed within 500 seconds (i.e. roughly 500 blocks) to not be considered stale + * + * For the sake of block explorer, these design decisions were made: + * - The signature values are not interesting. They're available in details but not worth displaying + * - Freshness is a word that isn't used anywhere else. It's meant to imply how close to the lower + * bound the signature was. But it doesn't matter as long as it's less than 500. + * @param param0 + * @returns + */ +export const TxDetailsHeartbeat = ({ + txData, + pubKey, + blockData, +}: TxDetailsHeartbeatProps) => { + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + + const cmd = txData.command as ValidatorHeartbeat; + + return ( + + + + {t('Node')} + + + + + + {t('Signed block height')} + + + + + + {t('Freshness (lower is better)')} + {scoreFreshness(txData.block, cmd.blockHeight)} + + + ); +}; diff --git a/apps/explorer/src/app/components/txs/details/tx-lp-amend.tsx b/apps/explorer/src/app/components/txs/details/tx-lp-amend.tsx new file mode 100644 index 000000000..b72403bf9 --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-lp-amend.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import type { + AmendLiquidityProvisionOrder, + 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'; + +interface TxDetailsOrderProps { + txData: BlockExplorerTransactionResult | undefined; + pubKey: string | undefined; + blockData: TendermintBlocksResponse | undefined; +} + +/** + * Specifies changes to the shape of a users Liquidity Commitment order for + * a specific market. So far this only displays the market, which is only + * because it's very easy to do so. + */ +export const TxDetailsLPAmend = ({ + txData, + pubKey, + blockData, +}: TxDetailsOrderProps) => { + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + + const cmd = txData.command as AmendLiquidityProvisionOrder; + + return ( + + + + {t('Market')} + + + + + + ); +}; diff --git a/apps/explorer/src/app/components/txs/details/tx-order.tsx b/apps/explorer/src/app/components/txs/details/tx-order.tsx new file mode 100644 index 000000000..3e0182b45 --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/tx-order.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { t } from '@vegaprotocol/react-helpers'; +import type { + BlockExplorerTransactionResult, + SubmitOrder, +} 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'; + +interface TxDetailsOrderProps { + txData: BlockExplorerTransactionResult | undefined; + pubKey: string | undefined; + blockData: TendermintBlocksResponse | undefined; +} + +/** + * An order type is probably the most interesting type we'll see! Except until: + * https://github.com/vegaprotocol/vega/issues/6832 is complete, we can only + * fetch the actual transaction and not more details about the order. So for now + * this view is very basic + */ +export const TxDetailsOrder = ({ + txData, + pubKey, + blockData, +}: TxDetailsOrderProps) => { + if (!txData) { + return <>{t('Awaiting Block Explorer transaction details')}; + } + + const cmd = txData.command as SubmitOrder; + + return ( + + + + {t('Market')} + + + + + + ); +}; diff --git a/apps/explorer/src/app/routes/blocks/id/block.spec.tsx b/apps/explorer/src/app/routes/blocks/id/block.spec.tsx index 140ab9102..89e91c9c1 100644 --- a/apps/explorer/src/app/routes/blocks/id/block.spec.tsx +++ b/apps/explorer/src/app/routes/blocks/id/block.spec.tsx @@ -3,6 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { Routes as RouteNames } from '../../route-names'; import { useFetch } from '@vegaprotocol/react-helpers'; +import { MockedProvider } from '@apollo/client/testing'; jest.mock('@vegaprotocol/react-helpers', () => { const original = jest.requireActual('@vegaprotocol/react-helpers'); @@ -121,11 +122,13 @@ const createBlockResponse = (id: number = blockId) => { const renderComponent = (id: number = blockId) => { return ( - - - } /> - - + + + + } /> + + + ); }; @@ -168,11 +171,13 @@ describe('Block', () => { expect(screen.getByTestId('block-header')).toHaveTextContent( `BLOCK ${blockId}` ); + const expectedValidator = '1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21'; const proposer = screen.getByTestId('block-validator'); - expect(proposer).toHaveTextContent( - '1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21' + expect(proposer).toHaveTextContent(expectedValidator); + expect(proposer).toHaveAttribute( + 'href', + `/${RouteNames.VALIDATORS}#${expectedValidator}` ); - expect(proposer).toHaveAttribute('href', `/${RouteNames.VALIDATORS}`); expect(screen.getByTestId('block-time')).toHaveTextContent( '59 minutes ago' ); diff --git a/apps/explorer/src/app/routes/blocks/id/block.tsx b/apps/explorer/src/app/routes/blocks/id/block.tsx index 20af5d386..5e86aff6e 100644 --- a/apps/explorer/src/app/routes/blocks/id/block.tsx +++ b/apps/explorer/src/app/routes/blocks/id/block.tsx @@ -15,8 +15,8 @@ import { TxsPerBlock } from '../../../components/txs/txs-per-block'; import { Button } from '@vegaprotocol/ui-toolkit'; import { Routes } from '../../route-names'; import { RenderFetched } from '../../../components/render-fetched'; -import { HighlightedLink } from '../../../components/highlighted-link'; import { t, useFetch } from '@vegaprotocol/react-helpers'; +import { NodeLink } from '../../../components/links'; const Block = () => { const { block } = useParams<{ block: string }>(); @@ -60,9 +60,8 @@ const Block = () => { Mined by - @@ -80,6 +79,12 @@ const Block = () => { )} + + Transactions + + {blockData.result.block.data.txs.length} + + {blockData.result.block.data.txs.length > 0 ? ( diff --git a/apps/explorer/src/app/routes/parties/id/__generated__/party-assets.ts b/apps/explorer/src/app/routes/parties/id/__generated__/party-assets.ts index e2d282ed5..1c060a657 100644 --- a/apps/explorer/src/app/routes/parties/id/__generated__/party-assets.ts +++ b/apps/explorer/src/app/routes/parties/id/__generated__/party-assets.ts @@ -8,7 +8,7 @@ export type ExplorerPartyAssetsQueryVariables = Types.Exact<{ }>; -export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } } } } | null> | null } | null } }> } | null }; +export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string } | null } } | null> | null } | null } }> } | null }; export const ExplorerPartyAssetsDocument = gql` @@ -49,6 +49,9 @@ export const ExplorerPartyAssetsDocument = gql` } type balance + market { + id + } } } } diff --git a/apps/explorer/src/app/routes/parties/id/index.tsx b/apps/explorer/src/app/routes/parties/id/index.tsx index f0bc13798..494c0b57b 100644 --- a/apps/explorer/src/app/routes/parties/id/index.tsx +++ b/apps/explorer/src/app/routes/parties/id/index.tsx @@ -69,9 +69,11 @@ const Party = () => { return (
-
+
- {t('Balance')} ({account.asset.symbol}) +

+ {t('Balance')} ({account.asset.symbol}) +

{addDecimalsFormatNumber( diff --git a/apps/explorer/src/app/routes/parties/id/party-assets.graphql b/apps/explorer/src/app/routes/parties/id/party-assets.graphql index 25f59dd2f..81a29db3b 100644 --- a/apps/explorer/src/app/routes/parties/id/party-assets.graphql +++ b/apps/explorer/src/app/routes/parties/id/party-assets.graphql @@ -35,6 +35,9 @@ query ExplorerPartyAssets($partyId: ID!) { } type balance + market { + id + } } } } diff --git a/apps/explorer/src/app/routes/txs/id/index.tsx b/apps/explorer/src/app/routes/txs/id/index.tsx index 27a3b4910..edce5fccb 100644 --- a/apps/explorer/src/app/routes/txs/id/index.tsx +++ b/apps/explorer/src/app/routes/txs/id/index.tsx @@ -3,7 +3,6 @@ import { Link, useParams } from 'react-router-dom'; import { useFetch } from '@vegaprotocol/react-helpers'; import { DATA_SOURCES } from '../../../config'; import { RenderFetched } from '../../../components/render-fetched'; -import { TxContent } from './tx-content'; import { TxDetails } from './tx-details'; import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response'; import { toNonHex } from '../../../components/search/detect-search'; @@ -36,24 +35,18 @@ const Tx = () => { - <> - - - - +
); diff --git a/apps/explorer/src/app/routes/txs/id/tx-content.tsx b/apps/explorer/src/app/routes/txs/id/tx-content.tsx index 9a5820f7d..62d4e71d2 100644 --- a/apps/explorer/src/app/routes/txs/id/tx-content.tsx +++ b/apps/explorer/src/app/routes/txs/id/tx-content.tsx @@ -1,6 +1,7 @@ import { t } from '@vegaprotocol/react-helpers'; import { StatusMessage } from '../../../components/status-message'; import { NestedDataList } from '../../../components/nested-data-list'; +import type { UnknownObject } from '../../../components/nested-data-list'; import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response'; interface TxContentProps { @@ -16,5 +17,5 @@ export const TxContent = ({ data }: TxContentProps) => { ); } - return ; + return ; }; diff --git a/apps/explorer/src/app/routes/txs/id/tx-details.spec.tsx b/apps/explorer/src/app/routes/txs/id/tx-details.spec.tsx index 5f8481ac8..7cd8d1072 100644 --- a/apps/explorer/src/app/routes/txs/id/tx-details.spec.tsx +++ b/apps/explorer/src/app/routes/txs/id/tx-details.spec.tsx @@ -1,9 +1,14 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { render, screen } from '@testing-library/react'; import { TxDetails } from './tx-details'; -import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response'; +import type { + BlockExplorerTransactionResult, + ValidatorHeartbeat, +} from '../../../routes/types/block-explorer-response'; -const pubKey = 'test'; +// Note: Long enough that there is a truncated output and a full output +const pubKey = + '67755549e43e95f0697f83b2bf419c6ccc18eee32a8a61b8ba6f59471b86fbef'; const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283'; const height = '52987'; @@ -15,7 +20,7 @@ const txData: BlockExplorerTransactionResult = { code: 0, cursor: `${height}.0`, type: 'type', - command: {}, + command: {} as ValidatorHeartbeat, }; const renderComponent = (txData: BlockExplorerTransactionResult) => ( @@ -25,13 +30,9 @@ const renderComponent = (txData: BlockExplorerTransactionResult) => ( ); describe('Transaction details', () => { - it('Renders the pubKey', () => { + it('Renders the details common to all txs', () => { render(renderComponent(txData)); expect(screen.getByText(pubKey)).toBeInTheDocument(); - }); - - it('Renders the height', () => { - render(renderComponent(txData)); - expect(screen.getByText(height)).toBeInTheDocument(); + expect(screen.getByText(hash)).toBeInTheDocument(); }); }); diff --git a/apps/explorer/src/app/routes/txs/id/tx-details.tsx b/apps/explorer/src/app/routes/txs/id/tx-details.tsx index 3127ee16e..3e32ce880 100644 --- a/apps/explorer/src/app/routes/txs/id/tx-details.tsx +++ b/apps/explorer/src/app/routes/txs/id/tx-details.tsx @@ -4,6 +4,7 @@ import type { BlockExplorerTransactionResult } from '../../../routes/types/block import React from 'react'; import { TruncateInline } from '../../../components/truncate/truncate'; import { Link } from 'react-router-dom'; +import { TxDetailsWrapper } from '../../../components/txs/details/tx-details-wrapper'; interface TxDetailsProps { txData: BlockExplorerTransactionResult | undefined; @@ -24,7 +25,7 @@ export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => { return (
-

+

{txData.type} by{' '} { {truncatedSubmitter}

-

- Block{' '} - - {txData.block} - - {', '} - Index {txData.index} -

+
); }; diff --git a/apps/explorer/src/app/routes/types/block-explorer-response.d.ts b/apps/explorer/src/app/routes/types/block-explorer-response.d.ts index 7682a644c..31bd0049a 100644 --- a/apps/explorer/src/app/routes/types/block-explorer-response.d.ts +++ b/apps/explorer/src/app/routes/types/block-explorer-response.d.ts @@ -1,3 +1,5 @@ +import type { UnknownObject } from '../../components/nested-data-list'; + export interface BlockExplorerTransactionResult { block: string; index: number; @@ -6,7 +8,13 @@ export interface BlockExplorerTransactionResult { type: string; code: number; cursor: string; - command: Record; + command: + | ValidatorHeartbeat + | SubmitOrder + | StateVariableProposal + | AmendLiquidityProvisionOrder + | BatchMarketInstructions + | ChainEvent; } export interface BlockExplorerTransactions { @@ -16,3 +24,95 @@ export interface BlockExplorerTransactions { export interface BlockExplorerTransaction { transaction: BlockExplorerTransactionResult; } + +export interface ValidatorHeartbeat { + blockHeight: string; + nonce: string; + validatorHeartbeat: { + nodeId: string; + ethereumSignature: ValidatorHeartbeatSignature; + vegaSignature: ValidatorHeartbeatSignature; + }; +} + +export interface ValidatorHeartbeatSignature { + algo: string; + value: string; + version: number; +} + +export interface SubmitOrder { + orderSubmission: { + marketId: string; + }; +} + +export interface StateVariableProposal { + proposal: { + stateVarId: string; + eventId: string; + kvb: StateVariableProposalValues[]; + }; +} + +export interface StateVariableProposalValues { + key: 'up' | 'down'; + tolerance: string; + value: UnknownObject; +} + +export interface AmendLiquidityProvisionOrder { + blockHeight: string; + nonce: string; + liquidityProvisionAmendment: { + marketId: string; + commitmentAmount: string; + fee: string; + sells: LiquidityProvisionOrderChange[]; + buys: LiquidityProvisionOrderChange[]; + reference: string; + }; +} + +export interface LiquidityProvisionOrderChange { + string: Reference; + proportion: number; + offset: string; +} + +export interface BatchMarketInstructions { + blockHeight: string; + nonce: string; + batchMarketInstructions: { + amendments: BatchInstruction[]; + submissions: BatchInstruction[]; + cancellations: BatchCancellationInstruction[]; + }; +} + +export interface BatchInstruction { + orderId: string; + marketId: string; +} + +export interface BatchCancellationInstruction { + orderId: string; + marketId: string; +} + +export interface ChainEvent { + blockHeight: string; + nonce: string; + chainEvent: { + erc20: { + deposit: ERC20Deposit; + }; + }; +} + +export interface ERC20Deposit { + vegaAssetId: string; + sourceEthereumAddress: string; + targetPartyId: string; + amount: string; +} diff --git a/apps/explorer/src/app/routes/validators/__generated__/nodes.ts b/apps/explorer/src/app/routes/validators/__generated__/nodes.ts index 1c4c65870..74be9bc0b 100644 --- a/apps/explorer/src/app/routes/validators/__generated__/nodes.ts +++ b/apps/explorer/src/app/routes/validators/__generated__/nodes.ts @@ -6,7 +6,7 @@ const defaultOptions = {} as const; export type ExplorerNodesQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, status: Types.NodeStatus, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } }; +export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } }; export const ExplorerNodesDocument = gql` @@ -22,6 +22,7 @@ export const ExplorerNodesDocument = gql` tmPubkey ethereumAddress location + status stakedByOperator stakedByDelegates stakedTotal @@ -31,8 +32,6 @@ export const ExplorerNodesDocument = gql` offline online } - status - name } } } diff --git a/apps/explorer/src/app/routes/validators/nodes.graphql b/apps/explorer/src/app/routes/validators/nodes.graphql index a5c824371..60c728722 100644 --- a/apps/explorer/src/app/routes/validators/nodes.graphql +++ b/apps/explorer/src/app/routes/validators/nodes.graphql @@ -10,6 +10,7 @@ query ExplorerNodes { tmPubkey ethereumAddress location + status stakedByOperator stakedByDelegates stakedTotal @@ -19,8 +20,6 @@ query ExplorerNodes { offline online } - status - name } } }