chore(#1976): block explorer txs list (#1996)

* Feat/1334 replace json (#1459)

* feat(explorer): add panel component

* feat(explorer): add info panel component

* feat(explorer): change layout styles

* feat(explorer): render json data in component for parties

* feat(explorer): change testnet env

* feat(explorer): change eslint json

* feat(explorer): render staking json in info panel component

* fix(explorer): pr comments

* fix(explorer): theme dark nav (#1498)

* fix(explorer): add party to consolidated search (#1588)

* fix(explorer): add party to consolidated search

* chore(explorer): add missing tests for detect-search.spec.ts

* feat(explorer): add txs infinite scroll list (#1625)

* Feat/1682 fix api endpoints (#1690)

* chore(explorer): replace tendermint api with block explorer api for transaction related searches

* feat(explorer): replace detect search type to be by process of elimination as temp measure

* fix(explorer): fix broken tests

* Feat/397 list transactions (#1770)

* chore(explorer): add stagnet 1 env vars

* feat(explorer): add info block component

* feat(explorer): change tx order type lozenge

* feat(explorer): change truncated-link.tsx style

* feat(explorer): add stats info component for txs

* feat(explorer): change page title size

* feat(explorer): update txs list and add txs for party

* fix(explorer): remove unused var

* fix(explorer): change copy and remove unused vars

* fix(explorer): pr review fixes

* Feat/1501 improve party details (#1785)

* feat(explorer): change styling party details page

* feat(explorer): use panel component in txs-stats-info.tsx

* feat(explorer): change order for party details sections

* chore(explorer): add stagnet-1 env vars (#1846)

* chore(explorer): add sandbox env vars (#1867)

* chore(explorer): update BE api urls (#1886)

* Feat/1416 tx detail (#1953)

* chore(explorer): add new vega colours to tw config

* feat(explorer): add new nested data list component

* feat(explorer): abstract page header with copy to new component

* feat(explorer): update styles tx details page

* fix(explorer): linting errors

* fix(explorer): fix txs type

* fix(explorer): fix strong typing

* fix(explorer): fix styling error for e2e test

* fix(explorer): reduce limit of txs from 100 to 20
This commit is contained in:
Elmar
2022-11-09 10:03:02 +00:00
committed by GitHub
parent 63bfcb6e75
commit 6d09e4ee64
55 changed files with 2556 additions and 557 deletions
+134 -6
View File
@@ -1,20 +1,148 @@
import '../support/common.functions';
context('Asset page', { tags: '@regression' }, function () {
before('gather system asset information', function () {
cy.get_asset_information().as('assetsInfo');
});
context('Asset page', function () {
describe('Verify elements on page', function () {
const assetsNavigation = 'a[href="/assets"]';
const assetHeader = '[data-testid="asset-header"]';
const jsonSection = '.language-json';
it('Assets page is displayed', function () {
before('Navigate to assets page', function () {
cy.visit('/');
cy.get(assetsNavigation).click();
cy.common_validate_blocks_data_displayed(assetHeader);
// Check we have enough enough assets
const assetNames = Object.keys(this.assetsInfo);
assert.isAtLeast(
assetNames.length,
5,
'Ensuring we have at least 5 assets to test'
);
});
it('Assets page displayed in mobile', function () {
it('should be able to see assets page sections', function () {
const assetNames = Object.keys(this.assetsInfo);
assetNames.forEach((assetName) => {
cy.get(assetHeader)
.contains(assetName)
.should('be.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
});
});
it('should be able to see all asset details displayed in JSON', function () {
const assetNames = Object.keys(this.assetsInfo);
assetNames.forEach((assetName) => {
cy.get(assetHeader)
.contains(assetName)
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((assetsListedInJson) => {
const assetInfo = this.assetsInfo[assetName];
assert.equal(assetsListedInJson.name, assetInfo.name);
assert.equal(assetsListedInJson.id, assetInfo.id);
assert.equal(assetsListedInJson.decimals, assetInfo.decimals);
assert.equal(assetsListedInJson.symbol, assetInfo.symbol);
assert.equal(
assetsListedInJson.source.__typename,
assetInfo.source.__typename
);
if (assetInfo.source.__typename == 'ERC20') {
assert.equal(
assetsListedInJson.source.contractAddress,
assetInfo.source.contractAddress
);
}
if (assetInfo.source.__typename == 'BuiltinAsset') {
assert.equal(
assetsListedInJson.source.maxFaucetAmountMint,
assetInfo.source.maxFaucetAmountMint
);
}
let knownAssetTypes = ['BuiltinAsset', 'ERC20'];
assert.include(
knownAssetTypes,
assetInfo.source.__typename,
`Checking that current asset type of ${assetInfo.source.__typename} /
is one of: ${knownAssetTypes}: /
If fail then we need to add extra tests for un-encountered asset types`
);
});
});
});
});
it('should be able to switch assets between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(assetsNavigation)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(assetsNavigation)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
it('should be able to see assets page displayed in mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(assetsNavigation).click();
cy.common_validate_blocks_data_displayed(assetHeader);
const assetNames = Object.keys(this.assetsInfo);
assetNames.forEach((assetName) => {
cy.get(assetHeader)
.contains(assetName)
.should('be.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
});
});
});
});
+252 -20
View File
@@ -1,35 +1,267 @@
import '../support/common.functions';
context('Network parameters page', function () {
before('visit token home page', function () {
cy.visit('/');
context('Network parameters page', { tags: '@smoke' }, function () {
before('navigate to network parameter page', function () {
cy.fixture('net_parameter_format_lookup').as('networkParameterFormat');
});
describe('Verify elements on page', function () {
const networkParametersNavigation = 'a[href="/network-parameters"]';
const networkParametersHeader = '[data-testid="network-param-header"]';
const tableRows = '[data-testid="key-value-table-row"]';
beforeEach('Navigate to network parameter page', function () {
before('navigate to network parameter page', function () {
cy.visit('/');
cy.get(networkParametersNavigation).click();
});
it('Network paremeter page is displayed', function () {
verifyNetworkParametersPageDisplayed();
it('should show network parameter heading at top of page', function () {
cy.get(networkParametersHeader)
.should('have.text', 'Network Parameters')
.and('be.visible');
});
it('Network parameter page displayed on mobile', function () {
it('should list each of the network parameters available', function () {
cy.get_network_parameters().then((network_parameters) => {
const numberOfNetworkParametersInSystem =
Object.keys(network_parameters).length;
cy.get(tableRows).should(
'have.length',
numberOfNetworkParametersInSystem
);
});
});
it('should list each network parameter displayed with json - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (this.networkParameterFormat.json.includes(parameterName)) {
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.invoke('text')
.convert_string_json_to_js_object()
.then((jsonOnPage) => {
cy.wrap(parameterValue, { log: false })
.convert_string_json_to_js_object()
.then((jsonInSystem) =>
assert.deepEqual(
jsonOnPage,
jsonInSystem,
`Checking ${parameterName} has the correct value of ${jsonInSystem}`
)
);
});
}
});
});
});
it('should list each network parameter displayed as a percentage - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (this.networkParameterFormat.percentage.includes(parameterName)) {
const formattedPercentageParameter =
(parseFloat(parameterValue) * 100).toFixed(0) + '%';
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.invoke('text')
.then((parameterValueOnPage) => {
assert.equal(
parameterValueOnPage,
formattedPercentageParameter,
`Checking ${parameterName} has the correct value of ${formattedPercentageParameter}`
);
cy.contains(parameterValueOnPage).should('be.visible');
});
}
});
});
});
it('should list each network parameter displayed as an id - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (this.networkParameterFormat.id.includes(parameterName)) {
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.should('contain', parameterValue)
.and('be.visible')
.invoke('text');
}
});
});
});
it('should list each network parameter displayed as a date - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (this.networkParameterFormat.date.includes(parameterName)) {
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.should('contain', parameterValue)
.and('be.visible');
}
});
});
});
it('should list each network parameter displayed as a duration - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (this.networkParameterFormat.duration.includes(parameterName)) {
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.should('contain', parameterValue)
.and('be.visible');
}
});
});
});
it('should list each network parameter displayed as a currency value with four decimals - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (this.networkParameterFormat.fiveDecimal.includes(parameterName)) {
cy.convert_number_to_four_decimal(parameterValue)
.add_commas_to_number_if_large_enough()
.then((parameterValueFormatted) => {
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.invoke('text')
.then((parameterValueOnPage) => {
assert.equal(
parameterValueOnPage,
parameterValueFormatted,
`Checking ${parameterName} has the correct value of ${parameterValueFormatted}`
);
cy.contains(parameterValueOnPage).should('be.visible');
});
});
}
});
});
});
it('should list each network parameter displayed as a currency value with eighteen decimals - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
const parameterValue = network_parameter[1];
if (
this.networkParameterFormat.eighteenDecimal.includes(parameterName)
) {
cy.convert_number_to_eighteen_decimal(parameterValue)
.add_commas_to_number_if_large_enough()
.then((parameterValueFormatted) => {
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.invoke('text')
.then((parameterValueOnPage) => {
assert.equal(
parameterValueOnPage,
parameterValueFormatted,
`Checking ${parameterName} has the correct value of ${parameterValueFormatted}`
);
cy.contains(parameterValueOnPage).should('be.visible');
});
});
}
});
});
});
it('should be able to switch network parameter page - between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
it('should be able to see network parameters - on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(networkParametersNavigation).click();
verifyNetworkParametersPageDisplayed();
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
const parameterName = network_parameter[0];
cy.get(tableRows)
.contains(parameterName)
.should('be.visible')
.next()
.should('not.be.empty')
.and('be.visible');
});
});
});
});
function verifyNetworkParametersPageDisplayed() {
cy.get('[data-testid="network-param-header"]').should(
'have.text',
'Network Parameters'
);
cy.common_verify_json_parameters(18);
cy.common_verify_json_string_values(6);
cy.common_verify_json_int_values(7);
}
});
@@ -0,0 +1,264 @@
const vegaWalletPublicKey = Cypress.env('vegaWalletPublicKey');
const partiesMenuHeader = 'a[href="/parties"]';
const partiesSearchBox = '[data-testid="party-input"]';
const partiesSearchAction = '[data-testid="go-submit"]';
const partiesJsonSection = '[data-testid="parties-json"]';
const txTimeout = Cypress.env('txTimeout');
let assetData = {
fUSDC: { id: 'fUSDC', name: 'USDC (fake)', amount: '10' },
fBTC: { id: 'fBTC', name: 'BTC (fake)', amount: '6' },
fEURO: { id: 'fEURO', name: 'EURO (fake)', amount: '8' },
fDAI: { id: 'fDAI', name: 'DAI (fake)', amount: '2' },
};
const assetsInTest = Object.keys(assetData);
context('Parties page', { tags: '@regression' }, function () {
before('send-faucet assets to connected vega wallet', function () {
cy.vega_wallet_import();
assetsInTest.forEach((asset) => {
cy.vega_wallet_receive_fauceted_asset(
assetData[asset].name,
assetData[asset].amount,
vegaWalletPublicKey
);
});
cy.visit('/');
});
describe('Verify parties page content', function () {
before('navigate to parties page and search for party', function () {
cy.get(partiesMenuHeader).click();
// Deliberate slow entry of party id/key - enabling transactions to sync
cy.get(partiesSearchBox).type(vegaWalletPublicKey, { delay: 120 });
cy.get(partiesSearchAction).click();
cy.get_connected_parties_accounts().as('party_accounts');
// Ensure balance of each party asset is correct
cy.get('@party_accounts').then((accounts) => {
assetsInTest.forEach((asset) => {
cy.get_asset_decimals(assetData[asset].id).then((assetDecimals) => {
assert.equal(
accounts[assetData[asset].id].balance,
assetData[asset].amount + assetDecimals,
`Checking ${assetData[asset].id} faucet was successfull`
);
});
});
});
});
it('should see party address id - having searched', function () {
cy.getByTestId('parties-header')
.siblings()
.contains(vegaWalletPublicKey.substring(0, 14))
.should('be.visible');
});
it('should see each asset and balance - within asset data section', function () {
assetsInTest.forEach((asset) => {
cy.contains(assetData[asset].name, txTimeout).should('be.visible');
cy.contains(assetData[asset].name)
.siblings()
.contains(assetData[asset].id)
.should('be.visible');
cy.contains(assetData[asset].name, txTimeout)
.parent()
.siblings()
.within(() => {
cy.get_asset_decimals(asset).then((assetDecimals) => {
cy.contains_exactly(
assetData[asset].amount + '.' + assetDecimals
).should('be.visible');
});
});
});
});
it(
'should be able to copy the party address id',
{ browser: 'chrome' },
function () {
cy.monitor_clipboard().as('clipboard');
cy.getByTestId('parties-header')
.next()
.within(() => {
cy.get('button').click();
});
cy.get('@clipboard')
.get_copied_text_from_clipboard()
.should('equal', vegaWalletPublicKey);
}
);
it(
'should be able to copy an asset id',
{ browser: 'chrome' },
function () {
cy.monitor_clipboard().as('clipboard');
cy.contains(assetData.fDAI.name, txTimeout).should('be.visible');
cy.contains(assetData.fDAI.name)
.parent()
.parent()
.siblings()
.within(() => {
cy.get('[data-state="closed"]').last().click({ force: true });
});
cy.get('@clipboard')
.get_copied_text_from_clipboard()
.should('equal', assetData.fDAI.id);
}
);
it('should be able to see JSON of each asset containing correct balance and decimals', function () {
cy.get(partiesJsonSection).should('be.visible');
assetsInTest.forEach((assetInTest) => {
cy.get(partiesJsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.get_party_accounts_data_from_js_object()
.then((accountsListedInJson) => {
cy.get_asset_information().then((assetsInfo) => {
const assetInfo =
assetsInfo[accountsListedInJson[assetInTest].asset.name];
assert.equal(
accountsListedInJson[assetInTest].asset.name,
assetInfo.name
);
assert.equal(
accountsListedInJson[assetInTest].asset.id,
assetInfo.id
);
assert.equal(
accountsListedInJson[assetInTest].asset.decimals,
assetInfo.decimals
);
assert.equal(
accountsListedInJson[assetInTest].asset.symbol,
assetInfo.symbol
);
assert.equal(
accountsListedInJson[assetInTest].asset.source.__typename,
assetInfo.source.__typename
);
cy.get_asset_decimals(assetInTest).then((assetDecimals) => {
assert.equal(
accountsListedInJson[assetInTest].balance,
assetData[assetInTest].amount + assetDecimals
);
});
});
});
});
});
it('should be able to switch parties page between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(partiesMenuHeader)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(partiesMenuHeader)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
after(
'teardown environment to prevent test data bleeding into other tests',
function () {
if (Cypress.env('TEARDOWN_NETWORK_AFTER_FLOWS')) {
cy.restart_vegacapsule_network();
}
}
);
Cypress.Commands.add('get_asset_decimals', (assetID) => {
cy.get_asset_information().then((assetsInfo) => {
const assetDecimals = assetsInfo[assetData[assetID].name].decimals;
let decimals = '';
for (let i = 0; i < assetDecimals; i++) decimals += '0';
return decimals;
});
});
Cypress.Commands.add('get_connected_parties_accounts', () => {
const mutation =
'{partiesConnection {edges{node{accountsConnection{edges{node{\
balance type asset {id symbol decimals}}}}}}}}';
cy.request({
method: 'POST',
url: `http://localhost:3028/query`,
body: {
query: mutation,
},
headers: { 'content-type': 'application/json' },
})
.its(
`body.data.partiesConnection.edges.1.node.accountsConnection.edges`
)
.then(function (response) {
let accounts = {};
response.forEach((account) => {
accounts[account.node.asset.id] = account.node;
});
return accounts;
});
});
Cypress.Commands.add(
'get_party_accounts_data_from_js_object',
{ prevSubject: true },
(jsObject) => {
const accounts = jsObject.party.accounts.reduce(function (
account,
entry
) {
account[entry.asset.id] = entry;
return account;
},
{});
return accounts;
}
);
});
});
+295 -22
View File
@@ -1,27 +1,300 @@
import '../support/common.functions';
context('Validator page', { tags: '@smoke' }, function () {
const validatorMenuHeading = 'a[href="/validators"]';
const tendermintDataHeader = '[data-testid="tendermint-header"]';
const vegaDataHeader = '[data-testid="vega-header"]';
const jsonSection = '.language-json';
before('Visit validators page and obtain data', function () {
cy.visit('/');
cy.get(validatorMenuHeading).click();
cy.get_validators().as('validators');
cy.get_nodes().as('nodes');
});
context('Validator page', function () {
describe('Verify elements on page', function () {
const validatorNavigation = 'a[href="/validators"]';
it('Validator page is displayed', function () {
cy.visit('/');
cy.get(validatorNavigation).click();
validateValidatorsDisplayed();
});
it('Validator page is displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(validatorNavigation).click();
validateValidatorsDisplayed();
});
function validateValidatorsDisplayed() {
cy.get('[data-testid="tendermint-header"]').should(
'have.text',
'Tendermint data'
before('Ensure at least two validators are present', function () {
assert.isAtLeast(
this.validators.length,
2,
'Ensuring at least two validators exist'
);
cy.get('[data-testid="tendermint-data"]').should('not.be.empty');
}
});
it('should be able to see validator page sections', function () {
cy.get(vegaDataHeader)
.contains('Vega data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
});
it('should be able to see relevant validator information in tendermint section', function () {
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((validatorsInJson) => {
this.validators.forEach((validator, index) => {
const validatorInJson =
validatorsInJson.result.validators[index];
assert.equal(
validatorInJson.address,
validator.address,
`Checking that validator address shown in json matches system data`
);
cy.contains(validator.address).should('be.visible');
assert.equal(
validatorInJson.pub_key.type,
validator.pub_key.type,
`Checking that validator public key type shown in json matches system data`
);
cy.contains(validator.pub_key.type).should('be.visible');
assert.equal(
validatorInJson.pub_key.value,
validator.pub_key.value,
`Checking that validator public key value shown in json matches system data`
);
cy.contains(validator.pub_key.value).should('be.visible');
assert.equal(
validatorInJson.voting_power,
validator.voting_power,
`Checking that validator voting power in json matches system data`
);
cy.contains(validator.voting_power).should('be.visible');
// Proposer priority can change frequently mid test
// Therefore only checking the field name is present.
cy.contains('proposer_priority').should('be.visible');
});
});
});
});
it('should be able to see relevant node information in vega data section', function () {
cy.get(vegaDataHeader)
.contains('Vega data')
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((nodesInJson) => {
this.nodes.forEach((node, index) => {
const nodeInJson = nodesInJson.nodes[index];
// Vegacapsule shows no info or null for following fields:
// name, infoURL, avatarUrl, location, epoch data
// Therefore, these values remain unchecked.
assert.equal(
nodeInJson.__typename,
node.__typename,
`Checking that node __typename shown in json matches system data`
);
cy.contains(node.__typename).should('be.visible');
assert.equal(
nodeInJson.id,
node.id,
`Checking that node id shown in json matches system data`
);
cy.contains(node.id).should('be.visible');
assert.equal(
nodeInJson.pubkey,
node.pubkey,
`Checking that node pubkey shown in json matches system data`
);
cy.contains(node.pubkey).should('be.visible');
assert.equal(
nodeInJson.tmPubkey,
node.tmPubkey,
`Checking that node tmPubkey shown in json matches system data`
);
cy.contains(node.tmPubkey).should('be.visible');
assert.equal(
nodeInJson.ethereumAddress,
node.ethereumAddress,
`Checking that node ethereumAddress shown in json matches system data`
);
cy.contains(node.ethereumAddress).should('be.visible');
assert.equal(
nodeInJson.stakedByOperator,
node.stakedByOperator,
`Checking that node stakedByOperator value shown in json matches system data`
);
cy.contains(node.stakedByOperator).should('be.visible');
assert.equal(
nodeInJson.stakedByDelegates,
node.stakedByDelegates,
`Checking that node stakedByDelegates value shown in json matches system data`
);
cy.contains(node.stakedByDelegates).should('be.visible');
assert.equal(
nodeInJson.stakedTotal,
node.stakedTotal,
`Checking that node stakedTotal shown in json matches system data`
);
cy.contains(node.stakedTotal).should('be.visible');
assert.equal(
nodeInJson.pendingStake,
node.pendingStake,
`Checking that node pendingStake shown in json matches system data`
);
cy.contains(node.pendingStake).should('be.visible');
assert.equal(
nodeInJson.status,
node.status,
`Checking that node status shown in json matches system data`
);
cy.contains(node.status).should('be.visible');
});
});
});
});
it('should be able to see validator page displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(validatorMenuHeading).click();
cy.get(vegaDataHeader)
.contains('Vega data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.next()
.within(() => {
this.validators.forEach((validator) => {
cy.contains(validator.address).should('be.visible');
cy.contains(validator.pub_key.type).should('be.visible');
cy.contains(validator.pub_key.value).should('be.visible');
cy.contains(validator.voting_power).should('be.visible');
// Proposer priority can change frequently mid test
// Therefore only checking the field name is present.
cy.contains('proposer_priority').should('be.visible');
});
});
});
it('should be able to switch validator page between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(validatorMenuHeading)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(validatorMenuHeading)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
Cypress.Commands.add('get_validators', () => {
cy.request({
method: 'GET',
url: `http://localhost:26617/validators`,
headers: { 'content-type': 'application/json' },
})
.its(`body.result.validators`)
.then(function (response) {
let validators = [];
response.forEach((account, index) => {
validators[index] = account;
});
return validators;
});
});
Cypress.Commands.add('get_nodes', () => {
const mutation =
'{nodes { id name infoUrl avatarUrl pubkey tmPubkey ethereumAddress \
location stakedByOperator stakedByDelegates stakedTotal pendingStake \
epochData { total offline online __typename } status name __typename}}';
cy.request({
method: 'POST',
url: `http://localhost:3028/query`,
body: {
query: mutation,
},
headers: { 'content-type': 'application/json' },
})
.its(`body.data.nodes`)
.then(function (response) {
let nodes = [];
response.forEach((node) => {
nodes.push(node);
});
return nodes;
});
});
});
});
+4 -2
View File
@@ -2,9 +2,11 @@ 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_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
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
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_BLOCK_EXPLORER=
# App flags
NX_EXPLORER_ASSETS=1
@@ -15,4 +17,4 @@ NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=1
NX_EXPLORER_TXS_LIST=0
+1 -1
View File
@@ -3,7 +3,7 @@ NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-exp
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
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
+2 -1
View File
@@ -4,6 +4,7 @@ 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='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
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
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
+2 -1
View File
@@ -4,6 +4,7 @@ NX_TENDERMINT_URL=https://mainnet-observer-proxy01.ops.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://mainnet-observer-proxy01.ops.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
NX_VEGA_URL=https://api.vega.xyz/query
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
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
NX_BLOCK_EXPLORER=
+11
View File
@@ -0,0 +1,11 @@
# 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
NX_VEGA_DOCS_URL=https://docs.vega.xyz/docs/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
+13
View File
@@ -0,0 +1,13 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
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
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
+2 -1
View File
@@ -4,6 +4,7 @@ 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='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
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
NX_BLOCK_EXPLORER=https://blockexplorer.n01.stagnet3.vega.xyz/rest
+3 -1
View File
@@ -1,8 +1,10 @@
# App configuration variables
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
NX_TENDERMINT_URL=https://tm.n07.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n07.testnet.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
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
+1
View File
@@ -5,3 +5,4 @@ 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=
+4 -1
View File
@@ -14,5 +14,8 @@
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
],
"env": {
"jest": true
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ function App() {
menuOpen && 'h-[100vh] overflow-hidden'
} antialiased m-0 bg-white dark:bg-black text-black dark:text-white`}
>
<div className="min-h-[100vh] max-w-[1300px] grid grid-rows-[repeat(2,_auto)_1fr] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)] md:grid-cols-[300px_1fr] border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r mx-auto">
<div className="grid grid-rows-[repeat(2,_auto)_1fr] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)] md:grid-cols-[300px_1fr] border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r mx-auto">
<Header
theme={theme}
toggleTheme={toggleTheme}
@@ -14,7 +14,12 @@ jest.mock('../search', () => ({
const renderComponent = () => (
<MemoryRouter>
<Header />
<Header
theme="dark"
toggleTheme={jest.fn()}
menuOpen={false}
setMenuOpen={jest.fn()}
/>
</MemoryRouter>
);
@@ -24,14 +24,15 @@ export const Header = ({
'md:col-span-2',
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
'p-4 gap-2 md:gap-4',
'border-b border-neutral-700 dark:border-neutral-300'
'border-b border-neutral-700 dark:border-neutral-300 bg-black',
'dark text-white'
);
return (
<header className={headerClasses}>
<div className="flex h-full items-center sm:items-stretch gap-4">
<Link to={Routes.HOME}>
<h1
className="text-3xl font-alpha uppercase calt mb-0"
className="text-white text-3xl font-alpha uppercase calt mb-0"
data-testid="explorer-header"
>
{t('Vega Explorer')}
@@ -41,13 +42,13 @@ export const Header = ({
</div>
<button
data-testid="open-menu"
className="md:hidden"
className="md:hidden text-white"
onClick={() => setMenuOpen(!menuOpen)}
>
<Icon name={menuOpen ? 'cross' : 'menu'} />
</button>
<Search />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} />
<ThemeSwitcher theme={theme} onToggle={toggleTheme} className="-my-4" />
</header>
);
};
@@ -0,0 +1 @@
export * from './info-block';
@@ -0,0 +1,29 @@
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import React from 'react';
export interface InfoBlockProps {
title: string;
subtitle: string;
tooltipInfo: string;
}
export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
return (
<div className="flex flex-col text-center ">
<h3 className="text-4xl">{title}</h3>
<p className="text-zinc-800 dark:text-zinc-300">
{subtitle}
{tooltipInfo ? (
<Tooltip description={tooltipInfo} align="center">
<span>
<Icon
name="info-sign"
className="ml-2 text-zinc-400 dark:text-zinc-600"
/>
</span>
</Tooltip>
) : undefined}
</p>
</div>
);
};
@@ -0,0 +1 @@
export * from './info-panel';
@@ -0,0 +1,81 @@
import React, { useMemo } from 'react';
import type { ReactNode } from 'react';
import { Panel } from '../panel';
import {
CopyWithTooltip,
Icon,
Intent,
Lozenge,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { TruncateInline } from '../truncate/truncate';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
interface InfoPanelProps {
children?: ReactNode | ReactNode[];
title: string;
id: string;
type?: string;
copy?: boolean;
}
export const InfoPanel = ({
children,
title,
id,
type,
copy = true,
}: InfoPanelProps) => {
const { screenSize } = useScreenDimensions();
const isTruncated = useMemo(
() => ['xs', 'sm', 'md', 'lg'].includes(screenSize),
[screenSize]
);
const isDesktop = useMemo(() => ['lg'].includes(screenSize), [screenSize]);
const visibleChars = useMemo(() => (isDesktop ? 20 : 10), [isDesktop]);
return (
<Panel>
<section className="flex justify-between items-center items-center">
<div>
<div className="flex gap-3 items-center">
<h3 className="text-lg font-medium">{title}</h3>
{isTruncated ? (
<TruncateInline
text={id}
startChars={visibleChars}
endChars={visibleChars}
className="text-black dark:text-zinc-200"
/>
) : (
<p
title={id}
className="text-black dark:text-zinc-200 truncate ..."
>
{id}
</p>
)}
{type && (
<Lozenge
className="text-xs leading-relaxed cursor-auto"
variant={Intent.None}
>
<Tooltip side="top" description={type} align="center">
<span>{type}</span>
</Tooltip>
</Lozenge>
)}
</div>
<div>{children}</div>
</div>
{copy && (
<CopyWithTooltip text={id}>
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" />
</button>
</CopyWithTooltip>
)}
</section>
</Panel>
);
};
@@ -0,0 +1 @@
export * from './nested-data-list';
@@ -0,0 +1,110 @@
import { render, waitFor } from '@testing-library/react';
import {
BORDER_COLOURS,
NestedDataList,
sortNestedDataByChildren,
} from './nested-data-list';
import userEvent from '@testing-library/user-event';
const mockData = {
nonce: '5980890939790185837',
validatorHeartbeat: {
nodeId: 'e07d2cd299659590c16ec1cc1c69936ad747083c379ea6b6cfeaa6e22c8af0cb',
ethereumSignature: {
value:
'8b157cb43a716ad541065f643e38cd92d7b1857c7beeb7d81e878be4f9a48f5a346be973e478eba3c18907888678625ba6700b086f65406d5ce0af0cae1d419300',
algo: 'eth',
version: 0,
},
vegaSignature: {
value:
'abffba6ddf07b3a732214dd780c35c94f7f0aeb9c5e9ce7d1a3ee641926a6ac568a2ecf0160c859633d327712c7a1b1590db4245d40646559a1ab2cc44d6fa01',
algo: 'vega/ed25519',
version: 0,
},
},
blockHeight: '174534',
};
describe('NestedDataList', () => {
it('should display the parent as a button', () => {
const tree = render(<NestedDataList data={mockData} />);
const { getAllByRole } = tree;
expect(getAllByRole('button')[0]).toHaveTextContent('Validator Heartbeat');
});
it('should display the children when a row is clicked', async () => {
const tree = render(<NestedDataList data={mockData} />);
const user = userEvent.setup();
const { getAllByRole } = tree;
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');
await user.click(expandBtn as HTMLButtonElement);
await waitFor(() => nestedContainer);
expect(nestedContainer).toHaveAttribute('aria-hidden', 'false');
});
it('add border to the title of the parent', () => {
const tree = render(<NestedDataList data={mockData} />);
const { getAllByRole } = tree;
const parent = getAllByRole('listitem', { name: 'Validator Heartbeat' });
expect(parent[0]).toHaveClass('pl-4 border-l-4');
});
it('add a border the children with the same colour', () => {
const tree = render(<NestedDataList data={mockData} />);
const { getAllByRole } = tree;
const parent = getAllByRole('listitem', { name: 'Validator Heartbeat' });
expect(parent[0].querySelector('li')).toHaveClass('pl-4 border-l-4 pt-2');
});
it('should repeat the border colours in the correct order', () => {
const colourMockData = {
t0: {
t1: {
t2: {
t3: {
t4: {
t5: {
t6: {
t7: {
t8: {
hello: 'world',
},
},
},
},
},
},
},
},
},
};
const tree = render(<NestedDataList data={colourMockData} />);
const { getByTestId } = tree;
for (let i = 0; i < 8; i++) {
const item = getByTestId(`T${i}`);
const expected = BORDER_COLOURS.dark[i % 5];
expect(item.style.borderColor.toUpperCase()).toBe(expected);
}
});
it('should sort the data by values with children', () => {
const mockData = {
nonce: '5980890939790185837',
validatorHeartbeat: {
nodeId:
'e07d2cd299659590c16ec1cc1c69936ad747083c379ea6b6cfeaa6e22c8af0cb',
someArray: ['0', '1', '2'],
},
blockHeight: '174534',
};
const expected = ['nonce', 'blockHeight', 'validatorHeartbeat'];
const result = sortNestedDataByChildren(mockData);
expect(result).toStrictEqual(expected);
});
});
@@ -0,0 +1,178 @@
import React, { useCallback, useContext, useMemo, useState } from 'react';
import classNames from 'classnames';
import isObject from 'lodash/isObject';
import { ThemeContext } from '@vegaprotocol/react-helpers';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import { VegaColours } from '@vegaprotocol/tailwindcss-config';
import isArray from 'lodash/isArray';
export type UnknownObject = Record<string, unknown>;
export type UnknownArray = unknown[];
interface NestedDataListProps {
data: UnknownObject | UnknownArray;
level?: number;
}
interface NestedDataListItemProps {
label: string;
value: unknown;
index: number;
}
export const BORDER_COLOURS = {
dark: [
VegaColours.pink.dark,
VegaColours.purple.dark,
VegaColours.green.dark,
VegaColours.blue.dark,
VegaColours.yellow.dark,
],
light: [
VegaColours.pink.DEFAULT,
VegaColours.purple.DEFAULT,
VegaColours.green.DEFAULT,
VegaColours.blue.DEFAULT,
VegaColours.yellow.DEFAULT,
],
};
const camelToTitle = (text: string) => {
const result = text.replace(/([A-Z])/g, ' $1');
return result.charAt(0).toUpperCase() + result.slice(1);
};
const getBorderColour = (index: number, theme: keyof typeof BORDER_COLOURS) => {
const colours = BORDER_COLOURS[theme];
const length = colours.length;
const modulo = index % length;
return colours[modulo];
};
const NestedDataListItem = ({
label,
value,
index,
}: NestedDataListItemProps) => {
const [isCollapsed, setCollapsed] = useState(true);
const toggleVisible = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
setCollapsed(!isCollapsed);
},
[isCollapsed]
);
const hasChildren = isObject(value) && !!Object.keys(value).length;
const title = useMemo(() => camelToTitle(label), [label]);
const theme = useContext(ThemeContext);
const currentLevelBorder = useMemo(
() => getBorderColour(index, theme),
[index, theme]
);
const nextLevelBorder = useMemo(
() => getBorderColour(index + 1, theme),
[index, theme]
);
const isArr = isArray(value);
const listItemClasses = classNames('pl-4 border-l-4', {
'pt-10 last:pb-0': hasChildren,
'first:pt-0': hasChildren && !index,
'pt-2': !hasChildren && index,
});
const titleClasses = classNames({
'text-xl pl-4 border-l-4 font-alpha': hasChildren,
'text-base font-medium whitespace-nowrap': !hasChildren,
});
return (
<li
data-testid={title}
title={title}
className={listItemClasses}
style={{ borderColor: currentLevelBorder }}
>
<div className="flex flex-wrap">
<h4 className={titleClasses} style={{ borderColor: nextLevelBorder }}>
{hasChildren ? (
<button
className="flex items-center gap-2"
type="button"
onClick={toggleVisible}
>
<span className="">{title}</span>
<small className="px-1 text-sm rounded bg-vega-light-200 dark:bg-vega-dark-200">
{isArr ? 'array' : typeof value}
</small>
<Icon
name={
isCollapsed ? IconNames.CHEVRON_DOWN : IconNames.CHEVRON_UP
}
/>
</button>
) : (
<span className="mr-2">{title}:</span>
)}
</h4>
{!hasChildren && (
<code className="text-vega-light-400 mb-2 last:mb-0 dark:text-vega-dark-400 break-all">
{JSON.stringify(value, null, ' ')}
</code>
)}
</div>
{hasChildren && (
<div aria-hidden={isCollapsed} className={isCollapsed ? 'hidden' : ''}>
<NestedDataList
data={value as UnknownObject | UnknownArray}
level={index + 1}
/>
</div>
)}
</li>
);
};
export const sortNestedDataByChildren = (data: UnknownObject | UnknownArray) =>
Object.keys(data)
.filter((key) => key)
.sort((a, b) => {
const isArr = isArray(data);
const valA = isArr ? data[+a] : data[a];
const valB = isArr ? data[+b] : data[b];
const hasChildrenA = isObject(valA) && !!Object.keys(valA).length;
const hasChildrenB = isObject(valB) && !!Object.keys(valB).length;
if (hasChildrenA && !hasChildrenB) {
return 1;
}
if (!hasChildrenA && hasChildrenB) {
return -1;
}
return 0;
});
export const NestedDataList = ({ data, level = 0 }: NestedDataListProps) => {
const nestedItems = useMemo(() => {
const sortedData = sortNestedDataByChildren(data);
const isArr = isArray(data);
if (sortedData.length) {
return sortedData.map((key) => (
<NestedDataListItem
key={key}
label={key}
value={isArr ? data[Number(key)] : data[key]}
index={level}
/>
));
}
return null;
}, [data, level]);
return <ul className="list-none">{nestedItems}</ul>;
};
@@ -0,0 +1 @@
export * from './page-header';
@@ -0,0 +1,49 @@
import React from 'react';
import type { ReactNode } from 'react';
import { TruncateInline } from '../truncate/truncate';
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
interface PageHeaderProps {
title: string;
truncateStart?: number;
truncateEnd?: number;
copy?: boolean;
prefix?: string | ReactNode;
className?: string;
}
export const PageHeader = ({
prefix,
title,
truncateStart,
truncateEnd,
copy = false,
className,
}: PageHeaderProps) => {
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha';
return (
<header className={className}>
<span className={`${titleClasses} block`}>{prefix}</span>
<div className="flex items-center gap-x-4">
<h2 className={titleClasses}>
{truncateStart && truncateEnd ? (
<TruncateInline
text={title}
startChars={truncateStart}
endChars={truncateEnd}
/>
) : (
title
)}
</h2>
{copy && (
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" className="" />
</button>
</CopyWithTooltip>
)}
</div>
</header>
);
};
@@ -0,0 +1 @@
export * from './panel';
@@ -0,0 +1,17 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
interface PanelProps {
children: ReactNode;
className?: string;
}
export const Panel = ({ children, className }: PanelProps) => (
<div
className={classNames(
'border border-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
className
)}
>
{children}
</div>
);
@@ -14,7 +14,7 @@ export const RouteTitle = ({
}: RouteTitleProps) => {
const classes = classnames(
'font-alpha',
'text-2xl',
'text-4xl',
'uppercase',
'mb-8',
className
@@ -0,0 +1,189 @@
import {
detectTypeByFetching,
detectTypeFromQuery,
getSearchType,
isBlock,
isHexadecimal,
isNetworkParty,
isNonHex,
SearchTypes,
toHex,
toNonHex,
} from './detect-search';
import { DATA_SOURCES } from '../../config';
global.fetch = jest.fn();
describe('Detect Search', () => {
it("should detect that it's a hexadecimal", () => {
const expected = true;
const testString =
'0x073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
const actual = isHexadecimal(testString);
expect(actual).toBe(expected);
});
it("should detect that it's not hexadecimal", () => {
const expected = true;
const testString =
'073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
const actual = isNonHex(testString);
expect(actual).toBe(expected);
});
it("should detect that it's a network party", () => {
const expected = true;
const testString = 'network';
const actual = isNetworkParty(testString);
expect(actual).toBe(expected);
});
it("should detect that it's a block", () => {
const expected = true;
const testString = '3188';
const actual = isBlock(testString);
expect(actual).toBe(expected);
});
it('should convert from non-hex to hex', () => {
const expected = '0x123';
const testString = '123';
const actual = toHex(testString);
expect(actual).toBe(expected);
});
it('should convert from hex to non-hex', () => {
const expected = '123';
const testString = '0x123';
const actual = toNonHex(testString);
expect(actual).toBe(expected);
});
it("should detect type client side from query if it's a hexadecimal", () => {
const expected = [SearchTypes.Party, SearchTypes.Transaction];
const testString =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a non hex", () => {
const expected = [SearchTypes.Party, SearchTypes.Transaction];
const testString =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a network party", () => {
const expected = [SearchTypes.Party];
const testString = 'network';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a block (number)", () => {
const expected = [SearchTypes.Block];
const testString = '23432';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
const query = '0xabc';
const type = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await detectTypeByFetching(query);
expect(fetch).toHaveBeenCalledWith(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
);
expect(result).toBe(type);
});
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
const query = 'abc';
const type = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await detectTypeByFetching(query);
expect(result).toBe(type);
});
it('getSearchType should return party from fetch response', async () => {
const query =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return transaction from fetch response', async () => {
const query =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return undefined from transaction response', async () => {
const query = 'u';
const expected = undefined;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return block if query is number', async () => {
const query = '123';
const expected = SearchTypes.Block;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return party if query is network', async () => {
const query = 'network';
const expected = SearchTypes.Party;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
});
@@ -0,0 +1,143 @@
import { DATA_SOURCES } from '../../config';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
export enum SearchTypes {
Transaction = 'transaction',
Party = 'party',
Block = 'block',
Order = 'order',
}
export const TX_LENGTH = 64;
export const isHexadecimal = (search: string) =>
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
export const isNonHex = (search: string) =>
!search.startsWith('0x') && search.length === TX_LENGTH;
export const isBlock = (search: string) => !Number.isNaN(Number(search));
export const isNetworkParty = (search: string) => search === 'network';
export const toHex = (query: string) =>
isHexadecimal(query) ? query : `0x${query}`;
export const toNonHex = (query: string) =>
isNonHex(query) ? query : `${query.replace('0x', '')}`;
export const detectTypeFromQuery = (
query: string
): SearchTypes[] | undefined => {
const i = query.toLowerCase();
if (isHexadecimal(i) || isNonHex(i)) {
return [SearchTypes.Party, SearchTypes.Transaction];
} else if (isNetworkParty(i)) {
return [SearchTypes.Party];
} else if (isBlock(i)) {
return [SearchTypes.Block];
}
return undefined;
};
export const detectTypeByFetching = async (
query: string
): Promise<SearchTypes | undefined> => {
const hash = toNonHex(query);
const request = await fetch(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
);
if (request?.ok) {
const body: BlockExplorerTransaction = await request.json();
if (body?.transaction) {
return SearchTypes.Transaction;
}
}
return SearchTypes.Party;
};
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
// export const detectTypeByFetching = async (
// query: string,
// type: SearchTypes
// ): Promise<SearchTypes | undefined> => {
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
//
// if (!TYPES.includes(type)) {
// throw new Error('Search type provided not recognised');
// }
//
// if (type === SearchTypes.Transaction) {
// const hash = toNonHex(query);
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
// );
//
// if (request?.ok) {
// const body: BlockExplorerTransaction = await request.json();
//
// if (body?.transaction) {
// return SearchTypes.Transaction;
// }
// }
// } else if (type === SearchTypes.Party) {
// const party = toNonHex(query);
//
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
// );
//
// if (request.ok) {
// const body: BlockExplorerTransactions = await request.json();
//
// if (body?.transactions?.length) {
// return SearchTypes.Party;
// }
// }
// }
//
// return undefined;
// };
// export const getSearchType = async (
// query: string
// ): Promise<SearchTypes | undefined> => {
// const searchTypes = detectTypeFromQuery(query);
// const hasResults = searchTypes?.length;
//
// if (hasResults) {
// if (hasResults > 1) {
// const promises = searchTypes.map((type) =>
// detectTypeByFetching(query, type)
// );
// const results = await Promise.all(promises);
// return results.find((result) => result !== undefined);
// }
//
// return searchTypes[0];
// }
//
// return undefined;
// };
export const getSearchType = async (
query: string
): Promise<SearchTypes | undefined> => {
const searchTypes = detectTypeFromQuery(query);
const hasResults = searchTypes?.length;
if (hasResults) {
if (hasResults > 1) {
return await detectTypeByFetching(query);
}
return searchTypes[0];
}
return undefined;
};
@@ -2,14 +2,23 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Search } from './search';
import { MemoryRouter } from 'react-router-dom';
import { Routes } from '../../routes/route-names';
import { SearchTypes, getSearchType } from './detect-search';
const mockedNavigate = jest.fn();
const mockGetSearchType = getSearchType as jest.MockedFunction<
typeof getSearchType
>;
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockedNavigate,
}));
jest.mock('./detect-search', () => ({
...jest.requireActual('./detect-search'),
getSearchType: jest.fn(),
}));
beforeEach(() => {
mockedNavigate.mockClear();
});
@@ -39,9 +48,10 @@ describe('Search', () => {
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
"Something doesn't look right"
'Transaction type is not recognised'
);
});
it('should render error if no input is given', async () => {
render(renderComponent());
const { button } = getInputs();
@@ -49,47 +59,14 @@ describe('Search', () => {
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
'Search required'
);
});
it('should render error if transaction is not hex', async () => {
render(renderComponent());
const { button, input } = getInputs();
fireEvent.change(input, {
target: {
value:
'0x123456789012345678901234567890123456789012345678901234567890123Q',
},
});
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
'Transaction is not hexadecimal'
);
});
it('should render error if transaction is not hex and does not have leading 0x', async () => {
render(renderComponent());
const { button, input } = getInputs();
fireEvent.change(input, {
target: {
value:
'123456789012345678901234567890123456789012345678901234567890123Q',
},
});
fireEvent.click(button);
expect(await screen.findByTestId('search-error')).toHaveTextContent(
'Transaction is not hexadecimal'
'Search query required'
);
});
it('should redirect to transactions page', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
fireEvent.change(input, {
target: {
value:
@@ -108,6 +85,7 @@ describe('Search', () => {
it('should redirect to transactions page without proceeding 0x', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
fireEvent.change(input, {
target: {
value:
@@ -123,9 +101,48 @@ describe('Search', () => {
});
});
it('should redirect to parties page', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
fireEvent.change(input, {
target: {
value:
'0x1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to parties page without proceeding 0x', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
fireEvent.change(input, {
target: {
value:
'1234567890123456789012345678901234567890123456789012345678901234',
},
});
fireEvent.click(button);
await waitFor(() => {
expect(mockedNavigate).toBeCalledWith(
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
);
});
});
it('should redirect to blocks page if passed a number', async () => {
render(renderComponent());
const { button, input } = getInputs();
mockGetSearchType.mockResolvedValue(SearchTypes.Block);
fireEvent.change(input, {
target: {
value: '123',
@@ -1,55 +1,54 @@
import React, { useCallback, useState } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import { Input, InputError, Button } from '@vegaprotocol/ui-toolkit';
import React from 'react';
import { Button, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { getSearchType, SearchTypes, toHex } from './detect-search';
import { Routes } from '../../routes/route-names';
const TX_LENGTH = 64;
interface FormFields {
search: string;
}
const isPrependedTransaction = (search: string) =>
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
const isTransaction = (search: string) =>
!search.startsWith('0x') && search.length === TX_LENGTH;
const isBlock = (search: string) => !Number.isNaN(Number(search));
export const Search = () => {
const { register, handleSubmit } = useForm<FormFields>();
const navigate = useNavigate();
const [error, setError] = React.useState<Error | null>(null);
const onSubmit = React.useCallback(
(fields: FormFields) => {
const [error, setError] = useState<Error | null>(null);
const onSubmit = useCallback(
async (fields: FormFields) => {
setError(null);
const search = fields.search;
if (!search) {
setError(new Error(t('Search required')));
} else if (isPrependedTransaction(search)) {
if (Number.isNaN(Number(search))) {
setError(new Error(t('Transaction is not hexadecimal')));
} else {
navigate(`${Routes.TX}/${search}`);
}
} else if (isTransaction(search)) {
if (Number.isNaN(Number(`0x${search}`))) {
setError(new Error(t('Transaction is not hexadecimal')));
} else {
navigate(`${Routes.TX}/0x${search}`);
}
} else if (isBlock(search)) {
navigate(`${Routes.BLOCKS}/${Number(search)}`);
} else {
setError(new Error(t("Something doesn't look right")));
const query = fields.search;
if (!query) {
return setError(new Error(t('Search query required')));
}
const result = await getSearchType(query);
const urlAsHex = toHex(query);
const unrecognisedError = new Error(
t('Transaction type is not recognised')
);
if (result) {
switch (result) {
case SearchTypes.Party:
return navigate(`${Routes.PARTIES}/${urlAsHex}`);
case SearchTypes.Transaction:
return navigate(`${Routes.TX}/${urlAsHex}`);
case SearchTypes.Block:
return navigate(`${Routes.BLOCKS}/${Number(query)}`);
default:
return setError(unrecognisedError);
}
}
return setError(unrecognisedError);
},
[navigate]
);
return (
<form
onSubmit={handleSubmit(onSubmit)}
@@ -59,21 +58,24 @@ export const Search = () => {
{t('Search by block number or transaction hash')}
</label>
<div className="flex items-stretch gap-2">
<Input
{...register('search')}
id="search"
data-testid="search"
hasError={Boolean(error?.message)}
type="text"
placeholder={t('Enter block number or transaction hash')}
/>
{error?.message && (
<div className="absolute top-[100%] flex-1 w-full">
<InputError data-testid="search-error" intent="danger">
{error.message}
</InputError>
</div>
)}
<div className="flex grow relative">
<Input
{...register('search')}
id="search"
data-testid="search"
className="text-white"
hasError={Boolean(error?.message)}
type="text"
placeholder={t('Enter block number, party id or transaction hash')}
/>
{error?.message && (
<div className="bg-white border border-t-0 border-accent absolute top-[100%] flex-1 w-full pb-2 px-2 rounded-b">
<InputError data-testid="search-error" intent="danger">
{error.message}
</InputError>
</div>
)}
</div>
<Button type="submit" size="sm" data-testid="search-button">
{t('Search')}
</Button>
@@ -20,7 +20,7 @@ export const TruncatedLink = ({
text={text}
startChars={startChars}
endChars={endChars}
className="font-mono font-bold underline"
className="underline uppercase underline-offset-2"
/>
</Link>
);
@@ -0,0 +1,35 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: TxsStats
// ====================================================
export interface TxsStats_statistics {
__typename: "Statistics";
/**
* Average number of orders added per blocks
*/
averageOrdersPerBlock: string;
/**
* Number of orders per seconds
*/
ordersPerSecond: string;
/**
* Number of transaction processed per block
*/
txPerBlock: string;
/**
* Number of the trades per seconds
*/
tradesPerSecond: string;
}
export interface TxsStats {
/**
* Get statistics about the Vega node
*/
statistics: TxsStats_statistics;
}
@@ -3,3 +3,4 @@ export { BlockTxsData } from './block-txs-data';
export { TxOrderType } from './tx-order-type';
export { TxsInfiniteList } from './txs-infinite-list';
export { TxsInfiniteListItem } from './txs-infinite-list-item';
export { TxsStatsInfo } from './txs-stats-info';
@@ -1,5 +1,3 @@
import { Lozenge, Intent } from '@vegaprotocol/ui-toolkit';
interface TxOrderTypeProps {
orderType: string;
className?: string;
@@ -34,10 +32,13 @@ const displayString: StringMap = {
ValidatorHeartbeat: 'Validator Heartbeat',
};
export const TxOrderType = ({ orderType, className }: TxOrderTypeProps) => {
export const TxOrderType = ({ orderType }: TxOrderTypeProps) => {
return (
<Lozenge data-testid="tx-type" variant={Intent.None} className={className}>
<div
data-testid="tx-type"
className="text-sm rounded-md leading-none px-2 py-2 inline-block text-white dark:text-white bg-zinc-800 dark:bg-zinc-800 "
>
{displayString[orderType] || orderType}
</Lozenge>
</div>
);
};
@@ -1,92 +1,89 @@
import { TxsInfiniteListItem } from './txs-infinite-list-item';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
describe('Txs infinite list item', () => {
it('should display "missing vital data" if "Type" data missing', () => {
it('should display "missing vital data" if "type" data missing', () => {
render(
<TxsInfiniteListItem
// @ts-ignore testing deliberate failure
Type={undefined}
Command={'test'}
Sig={'test'}
PubKey={'test'}
Nonce={1}
TxHash={'test'}
type={undefined}
submitter="test"
hash=""
index={0}
block="1"
/>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "Command" data missing', () => {
it('should display "missing vital data" if "hash" data missing', () => {
render(
<TxsInfiniteListItem
Type={'test'}
// @ts-ignore testing deliberate failure
Command={undefined}
Sig={'test'}
PubKey={'test'}
Nonce={1}
TxHash={'test'}
type="test"
submitter="test"
hash={undefined}
index={0}
block="1"
/>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "Pubkey" data missing', () => {
it('should display "missing vital data" if "submitter" data missing', () => {
render(
<TxsInfiniteListItem
Type={'test'}
Command={'test'}
Sig={'test'}
// @ts-ignore testing deliberate failure
PubKey={undefined}
Nonce={1}
TxHash={'test'}
type="test"
submitter={undefined}
hash="test"
index={0}
block="1"
/>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "TxHash" data missing', () => {
it('should display "missing vital data" if "block" data missing', () => {
render(
<TxsInfiniteListItem
Type={'test'}
Command={'test'}
Sig={'test'}
PubKey={'test'}
Nonce={1}
// @ts-ignore testing deliberate failure
TxHash={undefined}
type="test"
submitter="test"
hash="test"
index={0}
block={undefined}
/>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('should display "missing vital data" if "index" data missing', () => {
render(
<TxsInfiniteListItem
type="test"
submitter="test"
hash="test"
index={undefined}
block="1"
/>
);
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('renders data correctly', () => {
const testCommandData = JSON.stringify({
test: 'test of command data',
});
render(
<MemoryRouter>
<TxsInfiniteListItem
Type={'testType'}
Command={testCommandData}
Sig={'testSig'}
PubKey={'testPubKey'}
Nonce={1}
TxHash={'testTxHash'}
type="testType"
submitter="testPubKey"
hash="testTxHash"
index={1}
block="1"
/>
</MemoryRouter>
);
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('type')).toHaveTextContent('testType');
const button = screen.getByTestId('command-details');
act(() => {
fireEvent.click(button);
});
expect(screen.getByText('"test of command data"')).toBeInTheDocument();
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-index')).toHaveTextContent('1');
});
});
@@ -1,69 +1,79 @@
import React, { useState } from 'react';
import {
Dialog,
Icon,
Intent,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import React from 'react';
import { TruncatedLink } from '../truncate/truncated-link';
import { Routes } from '../../routes/route-names';
import { TxOrderType } from './tx-order-type';
import type { ChainExplorerTxResponse } from '../../routes/types/chain-explorer-response';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import { toHex } from '../search/detect-search';
const TRUNCATE_LENGTH = 14;
export const TxsInfiniteListItem = ({
TxHash,
PubKey,
Type,
Command,
}: ChainExplorerTxResponse) => {
const [open, setOpen] = useState(false);
if (!TxHash || !PubKey || !Type || !Command) {
hash,
submitter,
type,
block,
index,
}: Partial<BlockExplorerTransactionResult>) => {
if (
!hash ||
!submitter ||
!type ||
block === undefined ||
index === undefined
) {
return <div>Missing vital data</div>;
}
return (
<div
data-testid="transaction-row"
className="grid grid-cols-[repeat(2,_1fr)_240px] gap-12 w-full border-t border-neutral-600 dark:border-neutral-800 py-8 txs-infinite-list-item"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
>
<div className="whitespace-nowrap overflow-scroll" data-testid="tx-hash">
<div
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="tx-hash"
>
<span className="xl:hidden uppercase text-zinc-500">ID:&nbsp;</span>
<TruncatedLink
to={`/${Routes.TX}/${TxHash}`}
text={TxHash}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div className="whitespace-nowrap overflow-scroll" data-testid="pub-key">
<TruncatedLink
to={`/${Routes.PARTIES}/${PubKey}`}
text={PubKey}
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div
className="flex justify-between whitespace-nowrap overflow-scroll"
data-testid="type"
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="pub-key"
>
<TxOrderType orderType={Type} />
<button
title="More details"
onClick={() => setOpen(true)}
data-testid="command-details"
>
<Icon name="search-template" />
</button>
<Dialog
open={open}
onChange={(isOpen) => setOpen(false)}
intent={Intent.None}
>
<SyntaxHighlighter data={JSON.parse(Command)} />
</Dialog>
<span className="xl:hidden uppercase text-zinc-500">By:&nbsp;</span>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div className="text-sm col-span-5 xl:col-span-2 leading-none flex items-center">
<TxOrderType orderType={type} />
</div>
<div
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
data-testid="tx-block"
>
<span className="xl:hidden uppercase text-zinc-500">Block:&nbsp;</span>
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
data-testid="tx-index"
>
<span className="xl:hidden uppercase text-zinc-500">Index:&nbsp;</span>
{index}
</div>
</div>
);
@@ -1,18 +1,34 @@
import { TxsInfiniteList } from './txs-infinite-list';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
const generateTxs = (number: number) => {
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
return Array.from(Array(number)).map((_) => ({
Type: 'ChainEvent',
Command:
'{"txId":"0xc8941ac4ea989988cb8f72e8fdab2e2009376fd17619491439d36b519d27bc93","nonce":"1494","stakingEvent":{"index":"263","block":"14805346","stakeDeposited":{"ethereumAddress":"0x2e5fe63e5d49c26998cf4bfa9b64de1cf9ae7ef2","vegaPublicKey":"657c2a8a5867c43c831e24820b7544e2fdcc1cf610cfe0ece940fe78137400fd","amount":"38471116086510047870875","blockTime":"1652968806"}}}',
Sig: 'fe7624ab742c492cf1e667e79de4777992aca8e093c8707e1f22685c3125c6082cd21b85cd966a61ad4ca0cca2f8bed3082565caa5915bc3b2f78c1ae35cac0b',
PubKey:
'0x7d69327393cdfaaae50e5e215feca65273eafabfb38f32b8124e66298af346d5',
Nonce: 18296387398179254000,
TxHash:
'0x9C753FA6325F7A40D9C4FA5C25E24476C54613E12B1FA2DD841E3BB00D088B77',
block: '87901',
index: 2,
hash: '0F8B98DA0923A50786B852D9CA11E051CACC4C733E1DB93D535C7D81DBD10F6F',
submitter:
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
type: 'Submit Order',
code: 0,
cursor: '87901.2',
command: {
nonce: '4214037379192575529',
blockHeight: '87898',
orderSubmission: {
marketId:
'b4d0a070f5cc73a7d53b23d6f63f8cb52e937ed65d2469a3af4cc1e80e155fcf',
price: '14525946',
size: '54',
side: 'SIDE_SELL',
timeInForce: 'TIME_IN_FORCE_GTT',
expiresAt: '1664966445481288736',
type: 'TYPE_LIMIT',
reference: 'traderbot',
peggedOrder: null,
},
},
}));
};
@@ -41,13 +57,15 @@ describe('Txs infinite list', () => {
error={Error('test error!')}
/>
);
expect(screen.getByText('Error: test error!')).toBeInTheDocument();
expect(
screen.getByText('Cannot fetch transaction: Error: test error!')
).toBeInTheDocument();
});
it('item renders data of n length into list of n length', () => {
// Provided the number of items doesn't exceed the 30 it initially
// desires, all txs will initially render
const txs = generateTxs(10);
const txs = generateTxs(7);
render(
<MemoryRouter>
<TxsInfiniteList
@@ -64,7 +82,7 @@ describe('Txs infinite list', () => {
screen
.getByTestId('infinite-scroll-wrapper')
.querySelectorAll('.txs-infinite-list-item')
).toHaveLength(10);
).toHaveLength(7);
});
it('tries to load more items when required to initially fill the list', () => {
@@ -108,7 +126,7 @@ describe('Txs infinite list', () => {
});
it('loads more items is called when scrolled', () => {
const txs = generateTxs(20);
const txs = generateTxs(14);
const callback = jest.fn();
render(
@@ -125,7 +143,7 @@ describe('Txs infinite list', () => {
act(() => {
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
target: { scrollY: 600 },
target: { scrollY: 2000 },
});
});
@@ -1,21 +1,21 @@
import React from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/react-helpers';
import { t, useScreenDimensions } from '@vegaprotocol/react-helpers';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
import type { ChainExplorerTxResponse } from '../../routes/types/chain-explorer-response';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
interface TxsInfiniteListProps {
hasMoreTxs: boolean;
areTxsLoading: boolean | undefined;
txs: ChainExplorerTxResponse[] | undefined;
txs: BlockExplorerTransactionResult[] | undefined;
loadMoreTxs: () => void;
error: Error | undefined;
className?: string;
}
interface ItemProps {
index: ChainExplorerTxResponse;
index: BlockExplorerTransactionResult;
style: React.CSSProperties;
isLoading: boolean;
error: Error | undefined;
@@ -27,19 +27,19 @@ const NOOP = () => {};
const Item = ({ index, style, isLoading, error }: ItemProps) => {
let content;
if (error) {
content = t(`${error}`);
content = t(`Cannot fetch transaction: ${error}`);
} else if (isLoading) {
content = t('Loading...');
} else {
const { TxHash, PubKey, Type, Command, Sig, Nonce } = index;
const { hash, submitter, type, command, block, index: blockIndex } = index;
content = (
<TxsInfiniteListItem
Type={Type}
Command={Command}
Sig={Sig}
PubKey={PubKey}
Nonce={Nonce}
TxHash={TxHash}
type={type}
command={command}
submitter={submitter}
hash={hash}
block={block}
index={blockIndex}
/>
);
}
@@ -55,6 +55,9 @@ export const TxsInfiniteList = ({
error,
className,
}: TxsInfiniteListProps) => {
const { screenSize } = useScreenDimensions();
const isStacked = ['xs', 'sm', 'md', 'lg'].includes(screenSize);
if (!txs) {
return <div>No items</div>;
}
@@ -71,10 +74,15 @@ export const TxsInfiniteList = ({
return (
<div className={className} data-testid="transactions-list">
<div className="grid grid-cols-[repeat(2,_1fr)_240px] gap-12 w-full mb-8">
<div className="text-lg font-bold">Txn hash</div>
<div className="text-lg font-bold">Party</div>
<div className="text-lg font-bold pl-2">Type</div>
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-zinc-500 uppercase">
<div className="col-span-3">
<span className="hidden xl:inline">Transaction &nbsp;</span>
<span>ID</span>
</div>
<div className="col-span-3">Submitted By</div>
<div className="col-span-2">Type</div>
<div className="col-span-1">Block</div>
<div className="col-span-1">Index</div>
</div>
<div data-testid="infinite-scroll-wrapper">
<InfiniteLoader
@@ -87,7 +95,7 @@ export const TxsInfiniteList = ({
className="List"
height={595}
itemCount={itemCount}
itemSize={41}
itemSize={isStacked ? 134 : 72}
onItemsRendered={onItemsRendered}
ref={ref}
width={'100%'}
@@ -0,0 +1,80 @@
import { gql, useQuery } from '@apollo/client';
import { t } from '@vegaprotocol/react-helpers';
import { useEffect } from 'react';
import { InfoBlock } from '../../components/info-block';
import { Panel } from '../../components/panel';
import type { TxsStats, TxsStats_statistics } from './__generated__/TxsStats';
const STATS_QUERY = gql`
query TxsStats {
statistics {
averageOrdersPerBlock
ordersPerSecond
txPerBlock
tradesPerSecond
}
}
`;
interface StatsMap {
field: keyof TxsStats_statistics;
label: string;
info: string;
}
export const TXS_STATS_MAP: StatsMap[] = [
{
field: 'averageOrdersPerBlock',
label: t('Orders per block'),
info: t(
'Number of new orders processed in the last block. All orders derived from pegged orders and liquidity commitments count as a single order'
),
},
{
field: 'txPerBlock',
label: t('Transactions per block'),
info: t('Number of transactions processed in the last block'),
},
{
field: 'tradesPerSecond',
label: t('Trades per second'),
info: t('Number of trades processed in the last second'),
},
{
field: 'ordersPerSecond',
label: t('Order per second'),
info: t(
'Number of orders processed in the last second. All orders derived from pegged orders and liquidity commitments count as a single order'
),
},
];
interface TxsStatsInfoProps {
className?: string;
}
export const TxsStatsInfo = ({ className }: TxsStatsInfoProps) => {
const { data, startPolling, stopPolling } = useQuery<TxsStats>(STATS_QUERY);
useEffect(() => {
startPolling(1000);
return () => stopPolling();
});
const gridStyles =
'grid grid-rows-2 gap-4 grid-cols-2 xl:gap-8 xl:grid-rows-1 xl:grid-cols-4';
return (
<Panel className={className}>
<section className={gridStyles}>
{TXS_STATS_MAP.map((field) => (
<InfoBlock
subtitle={field.label}
tooltipInfo={field.info}
title={data?.statistics[field.field] || ''}
/>
))}
</section>
</Panel>
);
};
+1
View File
@@ -13,6 +13,7 @@ export const ENV = {
dsn: windowOrDefault('NX_EXPLORER_SENTRY_DSN'),
dataSources: {
chainExplorerUrl: windowOrDefault('NX_CHAIN_EXPLORER_URL'),
blockExplorerUrl: windowOrDefault('NX_BLOCK_EXPLORER'),
tendermintUrl: windowOrDefault('NX_TENDERMINT_URL'),
tendermintWebsocketUrl: windowOrDefault('NX_TENDERMINT_WEBSOCKET_URL'),
},
@@ -0,0 +1,91 @@
import { useCallback, useEffect, useState } from 'react';
import { useFetch } from '@vegaprotocol/react-helpers';
import type {
BlockExplorerTransactionResult,
BlockExplorerTransactions,
} from '../routes/types/block-explorer-response';
import { DATA_SOURCES } from '../config';
export interface TxsStateProps {
txsData: BlockExplorerTransactionResult[];
hasMoreTxs: boolean;
lastCursor: string;
}
export interface IUseTxsData {
limit?: number;
filters?: string;
}
interface IGetTxsDataUrl {
limit?: string;
filters?: string;
}
export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
const url = new URL(`${DATA_SOURCES.blockExplorerUrl}/transactions`);
if (limit) {
url.searchParams.append('limit', limit);
}
if (filters) {
url.searchParams.append('filters', filters);
}
return url;
};
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
const [{ txsData, hasMoreTxs, lastCursor }, setTxsState] =
useState<TxsStateProps>({
txsData: [],
hasMoreTxs: true,
lastCursor: '',
});
const url = getTxsDataUrl({ limit: limit?.toString(), filters });
const {
state: { data, error, loading },
refetch,
} = useFetch<BlockExplorerTransactions>(url.href, {}, false);
useEffect(() => {
if (data?.transactions?.length) {
setTxsState((prev) => ({
txsData: [...prev.txsData, ...data.transactions],
hasMoreTxs: true,
lastCursor:
data.transactions[data.transactions.length - 1].cursor || '',
}));
}
}, [setTxsState, data?.transactions]);
const loadTxs = useCallback(() => {
return refetch({
limit: limit,
before: lastCursor,
});
}, [lastCursor, limit, refetch]);
const refreshTxs = useCallback(async () => {
setTxsState((prev) => ({
...prev,
lastCursor: '',
hasMoreTxs: true,
txsData: [],
}));
}, [setTxsState]);
return {
data,
loading,
error,
txsData,
hasMoreTxs,
lastCursor,
refreshTxs,
loadTxs,
};
};
+113 -11
View File
@@ -1,17 +1,27 @@
import { useQuery } from '@apollo/client';
import { gql } from '@apollo/client';
import { t, useFetch } from '@vegaprotocol/react-helpers';
import React from 'react';
import {
t,
useFetch,
addDecimalsFormatNumber,
useScreenDimensions,
} from '@vegaprotocol/react-helpers';
import React, { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { RouteTitle } from '../../../components/route-title';
import { SubHeading } from '../../../components/sub-heading';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SyntaxHighlighter, AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Panel } from '../../../components/panel';
import { InfoPanel } from '../../../components/info-panel';
import { toNonHex } from '../../../components/search/detect-search';
import { DATA_SOURCES } from '../../../config';
import type { TendermintSearchTransactionResponse } from '../tendermint-transaction-response';
import type {
PartyAssetsQuery,
PartyAssetsQueryVariables,
} from './__generated__/PartyAssetsQuery';
import type { TendermintSearchTransactionResponse } from '../tendermint-transaction-response';
import { useTxsData } from '../../../hooks/use-txs-data';
import { TxsInfiniteList } from '../../../components/txs';
import { PageHeader } from '../../../components/page-header';
const PARTY_ASSETS_QUERY = gql`
query PartyAssetsQuery($partyId: ID!) {
@@ -50,13 +60,19 @@ const PARTY_ASSETS_QUERY = gql`
const Party = () => {
const { party } = useParams<{ party: string }>();
const partyId = party ? toNonHex(party) : '';
const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
const filters = `filters[tx.submitter]=${partyId}`;
const { hasMoreTxs, loadTxs, error, txsData, loading } = useTxsData({
limit: 10,
filters,
});
const {
state: { data: partyData },
} = useFetch<TendermintSearchTransactionResponse>(
`${
DATA_SOURCES.tendermintUrl
}/tx_search?query="tx.submitter='${party?.replace('0x', '')}'"`
`${DATA_SOURCES.tendermintUrl}/tx_search?query="tx.submitter='${partyId}'"`
);
const { data } = useQuery<PartyAssetsQuery, PartyAssetsQueryVariables>(
@@ -64,18 +80,104 @@ const Party = () => {
{
// Don't cache data for this query, party information can move quite quickly
fetchPolicy: 'network-only',
variables: { partyId: party?.replace('0x', '') || '' },
variables: { partyId },
skip: !party,
}
);
const header = data?.party?.id ? (
<PageHeader
title={data.party.id}
copy
truncateStart={visibleChars}
truncateEnd={visibleChars}
/>
) : (
<Panel>
<p>No party found for key {party}</p>
</Panel>
);
const accounts = (
<section>
{data?.party?.accounts?.length ? (
data.party.accounts.map((account) => {
return (
<InfoPanel title={account.asset.name} id={account.asset.id}>
<section>
<dl className="flex gap-2">
<dt className="text-zinc-500 dark:text-zinc-400 text-md">
{t('Balance')} ({account.asset.symbol})
</dt>
<dd className="text-md">
{addDecimalsFormatNumber(
account.balance,
account.asset.decimals
)}
</dd>
</dl>
</section>
</InfoPanel>
);
})
) : (
<Panel>
<p>No Data</p>
</Panel>
)}
</section>
);
const staking = (
<section>
{data?.party?.stake?.currentStakeAvailable ? (
<InfoPanel
title={t('Current Stake Available')}
id={data?.party?.stake?.currentStakeAvailable}
copy={false}
/>
) : (
<Panel>
<p>Nothing staked for {party}</p>
</Panel>
)}
</section>
);
return (
<section>
<RouteTitle data-testid="parties-header">{t('Party')}</RouteTitle>
<h1
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
data-testid="parties-header"
>
{t('Party')}
</h1>
{data ? (
<>
{header}
<SubHeading>{t('Asset data')}</SubHeading>
<SyntaxHighlighter data={data} />
{accounts}
<SubHeading>{t('Staking')}</SubHeading>
{staking}
<SubHeading>{t('JSON')}</SubHeading>
<section data-testid="parties-json">
<SyntaxHighlighter data={data} />
</section>
<AsyncRenderer
loading={loading as boolean}
error={error}
data={txsData}
>
<SubHeading>{t('Transactions')}</SubHeading>
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
txs={txsData}
loadMoreTxs={loadTxs}
error={error}
className="mb-28"
/>
</AsyncRenderer>
</>
) : null}
@@ -13,7 +13,7 @@ import Genesis from './genesis';
import { Block } from './blocks/id';
import { Blocks } from './blocks/home';
import { Tx } from './txs/id';
import { TxsHome, TxsHomeFallback } from './txs/home';
import { TxsList } from './txs/home';
import { PendingTxs } from './pending';
import flags from '../config/flags';
import { t } from '@vegaprotocol/react-helpers';
@@ -141,7 +141,7 @@ const routerConfig = [
},
{
index: true,
element: flags.txsList ? <TxsHome /> : <TxsHomeFallback />,
element: <TxsList />,
},
],
},
+10 -113
View File
@@ -1,86 +1,19 @@
import { DATA_SOURCES } from '../../../config';
import { useCallback, useState, useMemo } from 'react';
import { t, useFetch } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../../components/route-title';
import { BlocksRefetch } from '../../../components/blocks';
import { JumpToBlock } from '../../../components/jump-to-block';
import { TxsInfiniteList } from '../../../components/txs';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
import type { TendermintBlockchainResponse } from '../../blocks/tendermint-blockchain-response';
import { TxsInfiniteList, TxsStatsInfo } from '../../../components/txs';
import { useTxsData } from '../../../hooks/use-txs-data';
interface TxsProps {
latestBlockHeight: string;
}
interface TxsStateProps {
txsData: ChainExplorerTxResponse[];
hasMoreTxs: boolean;
nextPage: number;
}
const Txs = ({ latestBlockHeight }: TxsProps) => {
const [{ txsData, hasMoreTxs, nextPage }, setTxsState] =
useState<TxsStateProps>({
txsData: [],
hasMoreTxs: true,
nextPage: 1,
});
const reusedBodyParams = useMemo(
() => ({
node_url: DATA_SOURCES.tendermintUrl,
transaction_height: parseInt(latestBlockHeight),
page_size: 30,
}),
[latestBlockHeight]
);
const {
state: { error, loading },
refetch,
} = useFetch(
DATA_SOURCES.chainExplorerUrl,
{
method: 'POST',
body: JSON.stringify(reusedBodyParams),
},
false
);
const loadTxs = useCallback(async () => {
const data = await refetch(
undefined,
JSON.stringify({
...reusedBodyParams,
page_number: nextPage,
})
);
if (data) {
setTxsState((prev) => ({
...prev,
nextPage: prev.nextPage + 1,
hasMoreTxs: true,
txsData: [...prev.txsData, ...(data as ChainExplorerTxResponse[])],
}));
}
}, [nextPage, refetch, reusedBodyParams]);
const BE_TXS_PER_REQUEST = 20;
export const TxsList = () => {
const { hasMoreTxs, loadTxs, error, txsData, refreshTxs, loading } =
useTxsData({ limit: BE_TXS_PER_REQUEST });
return (
<section>
<section className="md:p-2 lg:p-4 xl:p-6">
<RouteTitle>{t('Transactions')}</RouteTitle>
<BlocksRefetch
refetch={() =>
refetch(
undefined,
JSON.stringify({
...reusedBodyParams,
page_number: 1,
})
)
}
/>
<BlocksRefetch refetch={refreshTxs} />
<TxsStatsInfo className="!my-12 py-8" />
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
@@ -89,42 +22,6 @@ const Txs = ({ latestBlockHeight }: TxsProps) => {
error={error}
className="mb-28"
/>
<JumpToBlock />
</section>
);
};
export const TxsHome = () => {
const {
state: { data, error, loading },
} = useFetch<TendermintBlockchainResponse>(
`${DATA_SOURCES.tendermintUrl}/blockchain`
);
return (
<AsyncRenderer
loading={!!loading}
loadingMessage={t('Getting latest block height...')}
error={error}
data={data}
noDataMessage={t('Could not get latest block height')}
render={(data) => (
<Txs
latestBlockHeight={
data?.result?.block_metas?.[0]?.header?.height || ''
}
/>
)}
/>
);
};
export const TxsHomeFallback = () => (
<>
<RouteTitle>{t('Transactions')}</RouteTitle>
<div>
The transactions list is currently disabled. Please use the search bar to
discover transaction data
</div>
</>
);
+38 -28
View File
@@ -1,49 +1,59 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { useFetch } from '@vegaprotocol/react-helpers';
import type { TendermintTransactionResponse } from '../tendermint-transaction-response.d';
import type { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
import { DATA_SOURCES } from '../../../config';
import { RouteTitle } from '../../../components/route-title';
import { RenderFetched } from '../../../components/render-fetched';
import { TxContent } from './tx-content';
import { TxDetails } from './tx-details';
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
import { toNonHex } from '../../../components/search/detect-search';
import { PageHeader } from '../../../components/page-header';
import { Routes } from '../../../routes/route-names';
import { IconNames } from '@blueprintjs/icons';
import { Icon } from '@vegaprotocol/ui-toolkit';
const Tx = () => {
const { txHash } = useParams<{ txHash: string }>();
const hash = txHash ? toNonHex(txHash) : '';
const {
state: { data: tTxData, loading: tTxLoading, error: tTxError },
} = useFetch<TendermintTransactionResponse>(
`${DATA_SOURCES.tendermintUrl}/tx?hash=${txHash}`
state: { data, loading: tTxLoading, error: tTxError },
} = useFetch<BlockExplorerTransaction>(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(hash)}`
);
const {
state: { data: ceTxData, loading: ceTxLoading, error: ceTxError },
} = useFetch<ChainExplorerTxResponse>(DATA_SOURCES.chainExplorerUrl, {
method: 'POST',
body: JSON.stringify({
tx_hash: txHash,
node_url: `${DATA_SOURCES.tendermintUrl}/`,
}),
});
return (
<section>
<RouteTitle>{t('Transaction details')}</RouteTitle>
<Link
className="font-normal underline underline-offset-4 block mb-5"
to={`/${Routes.TX}`}
>
<Icon
className="text-vega-light-300 dark:text-vega-light-300"
name={IconNames.CHEVRON_LEFT}
/>
All Transactions
</Link>
<PageHeader
title={hash}
prefix="Transaction"
copy
truncateStart={5}
truncateEnd={9}
className="mb-5"
/>
<RenderFetched error={tTxError} loading={tTxLoading}>
<TxDetails
className="mb-28"
txData={tTxData?.result}
pubKey={ceTxData?.PubKey}
/>
</RenderFetched>
<>
<TxDetails
className="mb-28"
txData={data?.transaction}
pubKey={data?.transaction.submitter}
/>
<h2 className="text-2xl uppercase mb-4">{t('Transaction content')}</h2>
<RenderFetched error={ceTxError} loading={ceTxLoading}>
<TxContent data={ceTxData} />
<TxContent data={data?.transaction} />
</>
</RenderFetched>
</section>
);
@@ -1,21 +1,14 @@
import { t } from '@vegaprotocol/react-helpers';
import { StatusMessage } from '../../../components/status-message';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import {
TableWithTbody,
TableCell,
TableHeader,
TableRow,
} from '../../../components/table';
import { TxOrderType } from '../../../components/txs';
import type { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
import { NestedDataList } from '../../../components/nested-data-list';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
interface TxContentProps {
data: ChainExplorerTxResponse | undefined;
data: BlockExplorerTransactionResult | undefined;
}
export const TxContent = ({ data }: TxContentProps) => {
if (!data?.Command) {
if (!data?.command) {
return (
<StatusMessage>
{t('Could not retrieve transaction content')}
@@ -23,21 +16,5 @@ export const TxContent = ({ data }: TxContentProps) => {
);
}
return (
<>
<TableWithTbody className="mb-12">
<TableRow modifier="bordered">
<TableHeader scope="row" className="w-[160px]">
{t('Type')}
</TableHeader>
<TableCell modifier="bordered">
<TxOrderType orderType={data.Type} />
</TableCell>
</TableRow>
</TableWithTbody>
<h3 className="font-mono mb-8">{t('Decoded transaction content')}</h3>
<SyntaxHighlighter data={JSON.parse(data.Command)} />
</>
);
return <NestedDataList data={data.command} />;
};
@@ -1,44 +1,30 @@
import { BrowserRouter as Router } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { truncateByChars } from '@vegaprotocol/react-helpers';
import { TxDetails, txDetailsTruncateLength } from './tx-details';
import type { Result } from '../tendermint-transaction-response.d';
import { TxDetails } from './tx-details';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
const pubKey = 'test';
const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283';
const height = '52987';
const tx =
'ClMIpeWjmKnn77FNEPedA5J9QhJAOGI3YjQzMWNlMmNhNzc4MWMzMTQ1M2IyYjc0MWYwMTJlNzQ1MzBhZDhjMDgzODVkMWQ1YjRiY2VkMTJiNDc1MhKTAQqAATM5NDFlNmExMzQ3MGVhNTlhNGExNmQzMjRiYzlkZjI5YWZkMzYxMDRiZjQ5MzEwZWMxM2ZiOTMxNTM2NGY3ZjU2ZTQyOTJmYTAyZDlhNTBlZDc0OWE0ZjExMzJiNjM2ZTZmMzQ3YzQ2NjdkYmM5OThmYzcyZjYzYzQxMzU4ZTAzEgx2ZWdhL2VkMjU1MTkYAYB9AtI+QDA5MzFhOGZkOGNjOTM1NDU4ZjQ3MGU0MzVhMDU0MTQzODdjZWE2ZjMyOWQ2NDhiZTg5NGZjZDQ0YmQ1MTdhMmI=';
const txData = {
const txData: BlockExplorerTransactionResult = {
hash,
height,
tx,
block: height,
index: 0,
tx_result: {
code: 0,
data: null,
log: '',
info: '',
events: [],
gas_wanted: '0',
gas_used: '0',
codespace: '',
},
submitter: pubKey,
code: 0,
cursor: `${height}.0`,
type: 'type',
command: {},
};
const renderComponent = (txData: Result) => (
const renderComponent = (txData: BlockExplorerTransactionResult) => (
<Router>
<TxDetails txData={txData} pubKey={pubKey} />
</Router>
);
describe('Transaction details', () => {
it('Renders the tx hash', () => {
render(renderComponent(txData));
expect(screen.getByText(hash)).toBeInTheDocument();
});
it('Renders the pubKey', () => {
render(renderComponent(txData));
expect(screen.getByText(pubKey)).toBeInTheDocument();
@@ -48,18 +34,4 @@ describe('Transaction details', () => {
render(renderComponent(txData));
expect(screen.getByText(height)).toBeInTheDocument();
});
it('Renders the truncated tx text', () => {
render(renderComponent(txData));
expect(
screen.getByText(
truncateByChars(tx, txDetailsTruncateLength, txDetailsTruncateLength)
)
).toBeInTheDocument();
});
it('Renders a copy button', () => {
render(renderComponent(txData));
expect(screen.getByTestId('copy-tx-to-clipboard')).toBeInTheDocument();
});
});
@@ -1,18 +1,12 @@
import { Routes } from '../../route-names';
import { ButtonLink, CopyWithTooltip } from '@vegaprotocol/ui-toolkit';
import {
TableWithTbody,
TableCell,
TableHeader,
TableRow,
} from '../../../components/table';
import { TruncateInline } from '../../../components/truncate/truncate';
import { t } from '@vegaprotocol/react-helpers';
import { HighlightedLink } from '../../../components/highlighted-link';
import type { Result } from '../tendermint-transaction-response.d';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import React from 'react';
import { TruncateInline } from '../../../components/truncate/truncate';
import { Link } from 'react-router-dom';
interface TxDetailsProps {
txData: Result | undefined;
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
className?: string;
}
@@ -21,54 +15,35 @@ export const txDetailsTruncateLength = 30;
export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => {
if (!txData) {
return <>{t('Awaiting Tendermint transaction details')}</>;
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const truncatedSubmitter = (
<TruncateInline text={pubKey || ''} startChars={5} endChars={5} />
);
return (
<TableWithTbody className={className}>
<TableRow modifier="bordered">
<TableCell>{t('Hash')}</TableCell>
<TableCell modifier="bordered" data-testid="hash">
{txData.hash}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableHeader scope="row" className="w-[160px]">
{t('Submitted by')}
</TableHeader>
<TableCell modifier="bordered" data-testid="submitted-by">
<HighlightedLink to={`/${Routes.PARTIES}/${pubKey}`} text={pubKey} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Block')}</TableCell>
<TableCell modifier="bordered" data-testid="block">
<HighlightedLink
to={`/${Routes.BLOCKS}/${txData.height}`}
text={txData.height}
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Encoded txn')}</TableCell>
<TableCell
modifier="bordered"
data-testid="encoded-tnx"
className="flex justify-between"
<section className="mb-10">
<h3 className="text-3xl xl:text-4xl uppercase font-alpha mb-4">
{txData.type} by{' '}
<Link
className="font-bold underline"
to={`/${Routes.PARTIES}/${pubKey}`}
>
<TruncateInline
text={txData.tx}
startChars={txDetailsTruncateLength}
endChars={txDetailsTruncateLength}
/>
<CopyWithTooltip text={txData.tx}>
<ButtonLink
title={t('Copy tx to clipboard')}
data-testid="copy-tx-to-clipboard"
/>
</CopyWithTooltip>
</TableCell>
</TableRow>
</TableWithTbody>
{truncatedSubmitter}
</Link>
</h3>
<p className="text-xl xl:text-2xl uppercase font-alpha">
Block{' '}
<Link
className="font-bold underline"
to={`/${Routes.BLOCKS}/${txData.block}`}
>
{txData.block}
</Link>
{', '}
Index {txData.index}
</p>
</section>
);
};
@@ -0,0 +1,18 @@
export interface BlockExplorerTransactionResult {
block: string;
index: number;
hash: string;
submitter: string;
type: string;
code: number;
cursor: string;
command: Record<string, unknown>;
}
export interface BlockExplorerTransactions {
transactions: BlockExplorerTransactionResult[];
}
export interface BlockExplorerTransaction {
transaction: BlockExplorerTransactionResult;
}
+9 -1
View File
@@ -1,6 +1,9 @@
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
const theme = require('../../libs/tailwindcss-config/src/theme');
const {
VegaColours,
} = require('../../libs/tailwindcss-config/src/vega-colours');
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
module.exports = {
@@ -11,7 +14,12 @@ module.exports = {
],
darkMode: 'class',
theme: {
extend: theme,
extend: {
...theme,
colors: {
vega: VegaColours,
},
},
},
plugins: [vegaCustomClasses],
};
+2
View File
@@ -1,9 +1,11 @@
const theme = require('./theme');
const themelite = require('./theme-lite');
const vegaCustomClasses = require('./vega-custom-classes');
const { VegaColours } = require('./vega-colours');
module.exports = {
theme,
themelite,
plugins: [vegaCustomClasses],
VegaColours,
};
@@ -0,0 +1,38 @@
const VegaColours = {
yellow: {
DEFAULT: '#D7FB50',
dark: '#9BE106',
},
pink: {
DEFAULT: '#FF077F',
dark: '#CF0064',
},
green: {
DEFAULT: '#00F780',
dark: '#00D46E',
},
blue: {
DEFAULT: '#0075FF',
dark: '#0046CD',
},
purple: {
DEFAULT: '#8028FF',
dark: '#5D0CD2',
},
dark: {
100: '#161616',
200: '#404040',
300: '#8B8B8B',
400: '#C0C0C0',
},
light: {
100: '#F0F0F0',
200: '#D2D2D2',
300: '#A7A7A7',
400: '#626262',
},
};
module.exports = {
VegaColours,
};