* 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:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -14,5 +14,8 @@
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: </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: </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: </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: </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 </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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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],
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user