Compare commits

...
Author SHA1 Message Date
Botond de21212e85 feat: add browser wallet connector 2023-02-24 15:00:20 +00:00
mattrussell36 86090c295c chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-10 06:05:53 +00:00
mattrussell36 6983587f28 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-10 00:10:28 +00:00
mattrussell36 5e87baf174 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-02-09 18:07:43 +00:00
ArtandEdd fc93bbd7c5 feat(explorer): asset details (#2882)
Co-authored-by: Edd <edd@vega.xyz>
2023-02-09 18:12:38 +01:00
Edd ce832ad6f4 feat(explorer): state variable proposal tx (#2837) 2023-02-09 15:29:46 +00:00
81 changed files with 1924 additions and 629 deletions
+45 -138
View File
@@ -1,150 +1,57 @@
context('Asset page', { tags: '@regression' }, function () {
before('gather system asset information', function () {
cy.get_asset_information().as('assetsInfo');
});
describe('Verify elements on page', function () {
const assetsNavigation = 'a[href="/assets"]';
const assetHeader = '[data-testid="asset-header"]';
const jsonSection = '.language-json';
before('Navigate to assets page', function () {
cy.visit('/');
cy.get(assetsNavigation).click();
context('Asset page', { tags: '@regression' }, () => {
const columns = ['symbol', 'name', 'id', 'type', 'status', 'actions'];
const hiddenOnMobile = ['id', 'type', 'status'];
describe('Verify elements on page', () => {
before('Navigate to assets page', () => {
cy.visit('/assets');
// 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('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');
});
cy.getAssets().then((assets) => {
assert.isAtLeast(
Object.keys(assets).length,
5,
'Ensuring we have at least 5 assets to test'
);
});
});
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.node.name);
assert.equal(assetsListedInJson.id, assetInfo.node.id);
assert.equal(
assetsListedInJson.decimals,
assetInfo.node.decimals
);
assert.equal(assetsListedInJson.symbol, assetInfo.node.symbol);
assert.equal(
assetsListedInJson.source.__typename,
assetInfo.node.source.__typename
);
if (assetInfo.node.source.__typename == 'ERC20') {
assert.equal(
assetsListedInJson.source.contractAddress,
assetInfo.node.source.contractAddress
);
}
if (assetInfo.node.source.__typename == 'BuiltinAsset') {
assert.equal(
assetsListedInJson.source.maxFaucetAmountMint,
assetInfo.node.source.maxFaucetAmountMint
);
}
let knownAssetTypes = ['BuiltinAsset', 'ERC20'];
assert.include(
knownAssetTypes,
assetInfo.node.source.__typename,
`Checking that current asset type of ${assetInfo.node.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();
it('should be able to see full assets list', () => {
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
});
// 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);
});
columns.forEach((col) => {
cy.get(`[col-id="${col}"]`).should('be.visible');
});
});
it('should be able to see assets page displayed in mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(assetsNavigation).click();
it('should be able to see assets page displayed in mobile', () => {
cy.switchToMobile();
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');
});
hiddenOnMobile.forEach((col) => {
cy.get(`[col-id="${col}"]`).should('have.length', 0);
});
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
});
});
});
it('should open details dialog when clicked on "View details"', () => {
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
.eq(0)
.should('contain.text', 'View details');
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
.eq(0)
.click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('dialog-close').click();
});
});
});
});
@@ -123,7 +123,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
.convert_string_json_to_js_object()
.get_party_accounts_data_from_js_object()
.then((accountsListedInJson) => {
cy.get_asset_information().then((assetsInfo) => {
cy.getAssets().then((assetsInfo) => {
const assetInfo =
assetsInfo[accountsListedInJson[assetInTest].asset.name];
@@ -205,7 +205,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
});
Cypress.Commands.add('get_asset_decimals', (assetID) => {
cy.get_asset_information().then((assetsInfo) => {
cy.getAssets().then((assetsInfo) => {
const assetDecimals = assetsInfo[assetData[assetID].name].decimals;
let decimals = '';
for (let i = 0; i < assetDecimals; i++) decimals += '0';
@@ -21,6 +21,10 @@ Cypress.Commands.add(
}
);
Cypress.Commands.add('switchToMobile', () => {
cy.viewport('iphone-x');
});
Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () {
cy.viewport('iphone-x');
cy.visit('/');
+19
View File
@@ -9,6 +9,23 @@ import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-web
import type { InMemoryCacheConfig } from '@apollo/client';
import { Footer } from './components/footer/footer';
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
return (
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
asJson={asJson}
open={isOpen}
onChange={setOpen}
/>
);
};
function App() {
const [menuOpen, setMenuOpen] = useState(false);
@@ -56,6 +73,8 @@ function App() {
<Main />
<Footer />
</div>
<DialogsContainer />
</NetworkLoader>
</TendermintWebsocketProvider>
);
@@ -1,6 +1,6 @@
import { useAssetDataProvider } from '@vegaprotocol/assets';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { AssetLink } from '../links';
import { useExplorerAssetQuery } from '../links/asset-link/__generated__/Asset';
export type AssetBalanceProps = {
assetId: string;
@@ -17,21 +17,17 @@ const AssetBalance = ({
price,
showAssetLink = true,
}: AssetBalanceProps) => {
const { data } = useExplorerAssetQuery({
variables: { id: assetId },
});
const { data: asset } = useAssetDataProvider(assetId);
const label =
data && data.asset?.decimals
? addDecimalsFormatNumber(price, data.asset.decimals)
asset && asset.decimals
? addDecimalsFormatNumber(price, asset.decimals)
: price;
return (
<div className="inline-block">
<span>{label}</span>{' '}
{showAssetLink && data?.asset?.id ? (
<AssetLink id={data.asset.id} />
) : null}
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null}
</div>
);
};
@@ -0,0 +1,28 @@
import { render, waitFor } from '@testing-library/react';
import { assetsList } from '../../mocks/assets';
import { AssetsTable } from './assets-table';
describe('AssetsTable', () => {
it('shows loading message on first render', async () => {
const res = render(<AssetsTable data={null} />);
expect(await res.findByText('Loading...')).toBeInTheDocument();
});
it('shows no data message if no assets found', async () => {
const res = render(<AssetsTable data={[]} />);
expect(
await res.findByText('This chain has no assets')
).toBeInTheDocument();
});
it('shows a table/list with all the assets', async () => {
const res = render(<AssetsTable data={assetsList} />);
await waitFor(() => {
const rowA1 = res.container.querySelector('[row-id="123"]');
expect(rowA1).toBeInTheDocument();
const rowA2 = res.container.querySelector('[row-id="456"]');
expect(rowA2).toBeInTheDocument();
});
});
});
@@ -0,0 +1,115 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/react-helpers';
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
};
export const AssetsTable = ({ data }: AssetsTableProps) => {
const openAssetDetailsDialog = useAssetDetailsDialogStore(
(state) => state.open
);
const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => {
ref.current?.columnApi.setColumnsVisible(
['id', 'type', 'status'],
window.innerWidth > BREAKPOINT_MD
);
};
useLayoutEffect(() => {
window.addEventListener('resize', showColumnsOnDesktop);
return () => {
window.removeEventListener('resize', showColumnsOnDesktop);
};
}, []);
return (
<AgGrid
ref={ref}
rowData={data}
getRowId={({ data }: { data: AssetFieldsFragment }) => data.id}
overlayNoRowsTemplate={t('This chain has no assets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
suppressCellFocus={true}
onGridReady={() => {
showColumnsOnDesktop();
}}
>
<AgGridColumn headerName={t('Symbol')} field="symbol" />
<AgGridColumn headerName={t('Name')} field="name" />
<AgGridColumn flex="2" headerName={t('ID')} field="id" />
<AgGridColumn
colId="type"
headerName={t('Type')}
field="source.__typename"
valueFormatter={({ value }: { value?: string }) =>
value && AssetTypeMapping[value].value
}
/>
<AgGridColumn
headerName={t('Status')}
field="status"
valueFormatter={({ value }: { value?: string }) =>
value && AssetStatusMapping[value].value
}
/>
<AgGridColumn
colId="actions"
headerName=""
sortable={false}
filter={false}
resizable={false}
wrapText={true}
field="id"
cellRenderer={({
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<div className="pb-1">
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value, e.target as HTMLElement);
}}
>
{t('View details')}
</ButtonLink>{' '}
<span className="max-md:hidden">
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(
value,
e.target as HTMLElement,
true
);
}}
>
{t('View JSON')}
</ButtonLink>
</span>
</div>
) : (
''
)
}
/>
</AgGrid>
);
};
@@ -1,8 +0,0 @@
query ExplorerAsset($id: ID!) {
asset(id: $id) {
id
name
status
decimals
}
}
@@ -1,51 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerAssetQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerAssetQuery = { __typename?: 'Query', asset?: { __typename?: 'Asset', id: string, name: string, status: Types.AssetStatus, decimals: number } | null };
export const ExplorerAssetDocument = gql`
query ExplorerAsset($id: ID!) {
asset(id: $id) {
id
name
status
decimals
}
}
`;
/**
* __useExplorerAssetQuery__
*
* To run a query within a React component, call `useExplorerAssetQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerAssetQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerAssetQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerAssetQuery(baseOptions: Apollo.QueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
}
export function useExplorerAssetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
}
export type ExplorerAssetQueryHookResult = ReturnType<typeof useExplorerAssetQuery>;
export type ExplorerAssetLazyQueryHookResult = ReturnType<typeof useExplorerAssetLazyQuery>;
export type ExplorerAssetQueryResult = Apollo.QueryResult<ExplorerAssetQuery, ExplorerAssetQueryVariables>;
@@ -1,63 +1,37 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import AssetLink from './asset-link';
import { ExplorerAssetDocument } from './__generated__/Asset';
import { render, waitFor } from '@testing-library/react';
import { AssetLink } from './asset-link';
import { mockAssetA1 } from '../../../mocks/assets';
function renderComponent(id: string, mock: MockedResponse[]) {
return (
<MockedProvider mocks={mock}>
<MockedProvider mocks={mock} addTypename={false}>
<MemoryRouter>
<AssetLink id={id} />
<AssetLink assetId={id} />
</MemoryRouter>
</MockedProvider>
);
}
describe('Asset link component', () => {
it('Renders the ID at first', () => {
describe('AssetLink', () => {
it('renders the asset id when not found and makes the button disabled', async () => {
const res = render(renderComponent('123', []));
expect(res.getByText('123')).toBeInTheDocument();
expect(await res.findByTestId('asset-link')).toBeDisabled();
await waitFor(async () => {
expect(await res.queryByText('A ONE')).toBeFalsy();
});
});
it('Renders the asset name when the query returns a result', async () => {
const mock = {
request: {
query: ExplorerAssetDocument,
variables: {
id: '123',
},
},
result: {
data: {
asset: {
id: '123',
name: 'test-label',
status: 'irrelevant-test-data',
decimals: 18,
},
},
},
};
const res = render(renderComponent('123', [mock]));
it('renders the asset name when found and make the button enabled', async () => {
const res = render(renderComponent('123', [mockAssetA1]));
expect(res.getByText('123')).toBeInTheDocument();
expect(await res.findByText('test-label')).toBeInTheDocument();
});
it('Leaves the asset id when the asset is not found', async () => {
const mock = {
request: {
query: ExplorerAssetDocument,
variables: {
id: '123',
},
},
error: new Error('No such asset'),
};
const res = render(renderComponent('123', [mock]));
expect(await res.findByText('123')).toBeInTheDocument();
await waitFor(async () => {
expect(await res.findByText('A ONE')).toBeInTheDocument();
expect(await res.findByTestId('asset-link')).not.toBeDisabled();
});
});
});
@@ -1,36 +1,35 @@
import React from 'react';
import { Routes } from '../../../routes/route-names';
import { useExplorerAssetQuery } from './__generated__/Asset';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import {
useAssetDataProvider,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
assetId: string;
};
/**
* Given an asset ID, it will fetch the asset name and show that,
* with a link to the assets list. If the name does not come back
* with a link to the assets modal. If the name does not come back
* it will use the ID instead.
*/
const AssetLink = ({ id, ...props }: AssetLinkProps) => {
const { data } = useExplorerAssetQuery({
variables: { id },
});
let label: string = id;
if (data?.asset?.name) {
label = data.asset.name;
}
export const AssetLink = ({ assetId, ...props }: AssetLinkProps) => {
const { data: asset } = useAssetDataProvider(assetId);
const open = useAssetDetailsDialogStore((state) => state.open);
const label = asset?.name ? asset.name : assetId;
return (
<Link className="underline" {...props} to={`/${Routes.ASSETS}#${id}`}>
<ButtonLink
data-testid="asset-link"
disabled={!asset}
onClick={(e) => {
open(assetId, e.target as HTMLElement);
}}
{...props}
>
<Hash text={label} />
</Link>
</ButtonLink>
);
};
export default AssetLink;
@@ -2,4 +2,4 @@ export { default as BlockLink } from './block-link/block-link';
export { default as PartyLink } from './party-link/party-link';
export { default as NodeLink } from './node-link/node-link';
export { default as MarketLink } from './market-link/market-link';
export { default as AssetLink } from './asset-link/asset-link';
export * from './asset-link/asset-link';
@@ -3,6 +3,7 @@ import React from 'react';
import classnames from 'classnames';
interface TableProps {
allowWrap?: boolean;
children: React.ReactNode;
className?: string;
}
@@ -25,8 +26,15 @@ interface TableCellProps extends ThHTMLAttributes<HTMLTableCellElement> {
modifier?: 'bordered' | 'background';
}
export const Table = ({ children, className, ...props }: TableProps) => {
const classes = classnames(className, 'overflow-x-auto whitespace-nowrap');
export const Table = ({
allowWrap,
children,
className,
...props
}: TableProps) => {
const classes = allowWrap
? className
: classnames(className, 'overflow-x-auto whitespace-nowrap');
return (
<div className={classes}>
<table className="w-full" {...props}>
@@ -37,11 +45,14 @@ export const Table = ({ children, className, ...props }: TableProps) => {
};
export const TableWithTbody = ({
allowWrap,
children,
className,
...props
}: TableProps) => {
const classes = classnames(className, 'overflow-x-auto whitespace-nowrap');
const classes = allowWrap
? className
: classnames(className, 'overflow-x-auto whitespace-nowrap');
return (
<div className={classes}>
<table className="w-full" {...props}>
@@ -76,9 +76,7 @@ describe('Chain Event: Builtin asset deposit', () => {
if (!assetLink.parentElement) {
throw new Error('Asset link does not exist');
}
expect(assetLink.parentElement.tagName).toEqual('A');
expect(assetLink.parentElement.getAttribute('href')).toEqual(
`/assets#${fullMock.vegaAssetId}`
);
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
});
});
@@ -34,7 +34,7 @@ export const TxDetailsChainEventBuiltinDeposit = ({
<TableRow modifier="bordered">
<TableCell>{t('Asset')}</TableCell>
<TableCell>
<AssetLink id={deposit.vegaAssetId} /> ({t('built in asset')})
<AssetLink assetId={deposit.vegaAssetId} /> ({t('built in asset')})
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@@ -82,9 +82,7 @@ describe('Chain Event: Builtin asset withdrawal', () => {
if (!assetLink.parentElement) {
throw new Error('Asset link does not exist');
}
expect(assetLink.parentElement.tagName).toEqual('A');
expect(assetLink.parentElement.getAttribute('href')).toEqual(
`/assets#${fullMock.vegaAssetId}`
);
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
});
});
@@ -39,8 +39,8 @@ export const TxDetailsChainEventBuiltinWithdrawal = ({
<TableRow modifier="bordered">
<TableCell>{t('Asset')}</TableCell>
<TableCell>
<AssetLink id={withdrawal.vegaAssetId || ''} /> ({t('built in asset')}
)
<AssetLink assetId={withdrawal.vegaAssetId || ''} /> (
{t('built in asset')})
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@@ -63,9 +63,7 @@ describe('Chain Event: ERC20 Asset Delist', () => {
if (!assetLink.parentElement) {
throw new Error('Asset link does not exist');
}
expect(assetLink.parentElement.tagName).toEqual('A');
expect(assetLink.parentElement.getAttribute('href')).toEqual(
`/assets#${fullMock.vegaAssetId}`
);
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
});
});
@@ -29,7 +29,7 @@ export const TxDetailsChainEventErc20AssetDelist = ({
<TableRow modifier="bordered">
<TableCell>{t('Removed Vega asset')}</TableCell>
<TableCell>
<AssetLink id={assetDelist.vegaAssetId || ''} />
<AssetLink assetId={assetDelist.vegaAssetId || ''} />
</TableCell>
</TableRow>
</>
@@ -79,10 +79,8 @@ describe('Chain Event: ERC20 Asset limits updated', () => {
if (!assetLink.parentElement) {
throw new Error('Asset link does not exist');
}
expect(assetLink.parentElement.tagName).toEqual('A');
expect(assetLink.parentElement.getAttribute('href')).toEqual(
`/assets#${fullMock.vegaAssetId}`
);
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
expect(screen.getByText(t('ERC20 asset'))).toBeInTheDocument();
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
@@ -51,7 +51,7 @@ export const TxDetailsChainEventErc20AssetLimitsUpdated = ({
<TableRow modifier="bordered">
<TableCell>{t('Vega asset')}</TableCell>
<TableCell>
<AssetLink id={assetLimitsUpdated.vegaAssetId} />
<AssetLink assetId={assetLimitsUpdated.vegaAssetId} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@@ -65,10 +65,8 @@ describe('Chain Event: ERC20 Asset List', () => {
if (!assetLink.parentElement) {
throw new Error('Asset link does not exist');
}
expect(assetLink.parentElement.tagName).toEqual('A');
expect(assetLink.parentElement.getAttribute('href')).toEqual(
`/assets#${fullMock.vegaAssetId}`
);
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
expect(screen.getByText(t('Source'))).toBeInTheDocument();
const ethLink = screen.getByText(`${fullMock.assetSource}`);
@@ -41,7 +41,7 @@ export const TxDetailsChainEventErc20AssetList = ({
<TableRow modifier="bordered">
<TableCell>{t('Added Vega asset')}</TableCell>
<TableCell>
<AssetLink id={assetList.vegaAssetId} />
<AssetLink assetId={assetList.vegaAssetId} />
</TableCell>
</TableRow>
</>
@@ -75,10 +75,8 @@ describe('Chain Event: ERC20 asset deposit', () => {
if (!assetLink.parentElement) {
throw new Error('Asset link does not exist');
}
expect(assetLink.parentElement.tagName).toEqual('A');
expect(assetLink.parentElement.getAttribute('href')).toEqual(
`/assets#${fullMock.vegaAssetId}`
);
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
expect(screen.getByText(t('Source'))).toBeInTheDocument();
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
@@ -51,7 +51,7 @@ export const TxDetailsChainEventDeposit = ({
<TableRow modifier="bordered">
<TableCell>{t('Asset')}</TableCell>
<TableCell>
<AssetLink id={deposit.vegaAssetId} />
<AssetLink assetId={deposit.vegaAssetId} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@@ -60,10 +60,8 @@ describe('Chain Event: ERC20 asset deposit', () => {
if (!assetLink.parentElement) {
throw new Error('Asset link does not exist');
}
expect(assetLink.parentElement.tagName).toEqual('A');
expect(assetLink.parentElement.getAttribute('href')).toEqual(
`/assets#${fullMock.vegaAssetId}`
);
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
const ethLink = screen.getByText(`${fullMock.targetEthereumAddress}`);
@@ -45,7 +45,7 @@ export const TxDetailsChainEventWithdrawal = ({
<TableRow modifier="bordered">
<TableCell>{t('Asset')}</TableCell>
<TableCell>
<AssetLink id={withdrawal.vegaAssetId} />
<AssetLink assetId={withdrawal.vegaAssetId} />
</TableCell>
</TableRow>
</>
@@ -0,0 +1,108 @@
import { getValues } from './bound-factors';
import type { components } from '../../../../../types/explorer';
type KeyValueBundle = components['schemas']['vegaKeyValueBundle'][];
describe('getValues', () => {
it('handles an empty array by returning a dashed template', () => {
const res = getValues([]);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '-');
expect(res.up).toHaveProperty('value', '-');
});
it('handles undefined', () => {
const res = getValues(undefined as unknown as KeyValueBundle);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '-');
expect(res.up).toHaveProperty('value', '-');
});
it('handles a kvb that only has one side (should not happen)', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { vectorVal: { value: ['0.123'] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '0.123');
});
it('handles a kvb that has a matrixVal instead of a scalarval by ignoring it', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { matrixVal: { value: [{ value: ['0.123'] }] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '-');
expect(res.down).toHaveProperty('value', '-');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '-');
});
it('ignores unexpected extra values in the kvb', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { vectorVal: { value: ['0.123', '0.77'] } },
},
{
key: 'down',
tolerance: '0.001',
value: { vectorVal: { value: ['0.321'] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '0.001');
expect(res.down).toHaveProperty('value', '0.321');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '0.123');
});
it('handles a full kvb', () => {
const k: KeyValueBundle = [
{
key: 'up',
tolerance: '0.1',
value: { vectorVal: { value: ['0.123'] } },
},
{
key: 'down',
tolerance: '0.001',
value: { vectorVal: { value: ['0.321'] } },
},
];
const res = getValues(k);
expect(res).toHaveProperty('down');
expect(res.down).toHaveProperty('tolerance', '0.001');
expect(res.down).toHaveProperty('value', '0.321');
expect(res).toHaveProperty('up');
expect(res.up).toHaveProperty('tolerance', '0.1');
expect(res.up).toHaveProperty('value', '0.123');
});
});
@@ -0,0 +1,87 @@
import { t } from '@vegaprotocol/react-helpers';
import type { components } from '../../../../../types/explorer';
import { Table, TableRow, TableHeader, TableCell } from '../../../table';
interface StateVariableProposalBoundFactorsProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* A dumb as rocks function completely tied to what the structure of this variable should be
* @param kvb The key/value bundle
* @returns Object
*/
export function getValues(kvb: StateVariableProposalBoundFactorsProps['kvb']) {
const template = {
up: {
tolerance: '-',
value: '-',
},
down: {
tolerance: '-',
value: '-',
},
};
if (kvb && kvb.length > 0) {
kvb.forEach((v) => {
if (v.key === 'up') {
template.up.tolerance = v.tolerance || '-';
template.up.value = v.value?.vectorVal?.value
? v.value?.vectorVal.value[0]
: '-';
} else if (v.key === 'down') {
template.down.tolerance = v.tolerance || '-';
template.down.value = v.value?.vectorVal?.value
? v.value?.vectorVal.value[0]
: '-';
}
});
}
return template;
}
/**
* State Variable proposals updating Bound Factors. This contains two bundles,
* an up vector and a down vector
*
* This is nearly identical to risk factors.
*/
export const StateVariableProposalBoundFactors = ({
kvb,
}: StateVariableProposalBoundFactorsProps) => {
const v = getValues(kvb);
return (
<Table allowWrap={true} className="w-1/3">
<thead>
<TableRow modifier="bordered">
<TableHeader align="left">{t('Parameter')}</TableHeader>
<TableHeader align="center">{t('New value')}</TableHeader>
<TableHeader align="right">{t('Tolerance')}</TableHeader>
</TableRow>
</thead>
<tbody>
<TableRow modifier="bordered">
<TableCell>{t('Up')}</TableCell>
<TableCell align="right" className="font-mono">
{v.up.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.up.tolerance}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Down')}</TableCell>
<TableCell align="right" className="font-mono">
{v.down.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.down.tolerance}
</TableCell>
</TableRow>
</tbody>
</Table>
);
};
@@ -0,0 +1,31 @@
import type { components } from '../../../../../types/explorer';
import { StateVariableProposalUnknown } from './unknown';
import { StateVariableProposalBoundFactors } from './bound-factors';
import { StateVariableProposalRiskFactors } from './risk-factors';
interface StateVariableProposalWrapperProps {
stateVariable: string | undefined;
kvb: readonly components['schemas']['vegaKeyValueBundle'][] | undefined;
}
/**
* State Variable proposals
*/
export const StateVariableProposalWrapper = ({
stateVariable,
kvb,
}: StateVariableProposalWrapperProps) => {
if (!stateVariable || !kvb || kvb.length === 0) {
return null;
}
if (stateVariable.indexOf('bound-factors') !== -1) {
return <StateVariableProposalBoundFactors kvb={kvb} />;
} else if (stateVariable.indexOf('risk-factors') !== -1) {
return <StateVariableProposalRiskFactors kvb={kvb} />;
} else if (stateVariable.indexOf('probability_of_trading') !== -1) {
return <StateVariableProposalRiskFactors kvb={kvb} />;
} else {
return <StateVariableProposalUnknown kvb={kvb} />;
}
};
@@ -0,0 +1,124 @@
import { t } from '@vegaprotocol/react-helpers';
import zip from 'lodash/zip';
import type { components } from '../../../../../types/explorer';
import { Table, TableRow, TableHeader, TableCell } from '../../../table';
import { StateVariableProposalUnknown } from './unknown';
interface StateVariableProposalRiskFactorsProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* A dumb as rocks function completely tied to what the structure of this variable should be
*
* @param kvb The key/value bundle
* @returns Object
*/
export function getValues(kvb: StateVariableProposalRiskFactorsProps['kvb']) {
try {
const template = {
bid: {
offsetTolerance: '-',
probabilityTolerance: '-',
offset: [] as Readonly<string[]>,
probability: [] as Readonly<string[]>,
rows: [] as [string | undefined, string | undefined][],
},
ask: {
offsetTolerance: '-',
probabilityTolerance: '-',
offset: [] as Readonly<string[]>,
probability: [] as Readonly<string[]>,
rows: [] as [string | undefined, string | undefined][],
},
};
kvb.forEach((v) => {
if (v.key === 'bidOffset') {
template.bid.offsetTolerance = v.tolerance || '-';
template.bid.offset = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
} else if (v.key === 'bidProbability') {
template.bid.probabilityTolerance = v.tolerance || '-';
template.bid.probability = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
} else if (v.key === 'askOffset') {
template.ask.offsetTolerance = v.tolerance || '-';
template.ask.offset = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
} else if (v.key === 'askProbability') {
template.ask.probabilityTolerance = v.tolerance || '-';
template.ask.probability = v.value?.vectorVal?.value
? v.value?.vectorVal?.value
: ['0'];
}
});
// Bundles up offset and probability in to a row
if (template.bid.offset.length > 0 && template.bid.probability.length > 0) {
template.bid.rows = zip(template.bid.offset, template.bid.probability);
}
if (template.ask.offset.length > 0 && template.ask.probability.length > 0) {
template.ask.rows = zip(template.ask.offset, template.ask.probability);
}
return template;
} catch (e) {
// This will result in the table not being rendered
return null;
}
}
/**
* State Variable proposals updating Risk Factors. This contains two bundles,
* a long vector and a short vector
*/
export const StateVariableProposalRiskFactors = ({
kvb,
}: StateVariableProposalRiskFactorsProps) => {
const v = getValues(kvb);
const all = v ? zip(v.bid.rows, v.ask.rows) : [];
if (all.length === 0) {
// Give up, do a JSON view
return <StateVariableProposalUnknown kvb={kvb} />;
}
return (
<Table allowWrap={true} className="text-xs lg:text-base max-w-2xl">
<thead>
<TableRow modifier="bordered">
<TableHeader align="left">{t('Mid offset')}</TableHeader>
<TableHeader align="right">{t('Bid probability')}</TableHeader>
<TableHeader align="right" className="pl-2">
{t('Ask probability')}
</TableHeader>
</TableRow>
</thead>
<tbody>
{all.map((r) => {
// Simple remapping of the data to protect against undefineds
const row = {
o: r[0] ? r[0][0] : r[1] ? r[1][0] : '-',
b: r[0] ? r[0][1] : '-',
a: r[1] ? r[1][1] : '-',
};
return (
<TableRow key={`${row.o}${row.b}${row.a}`}>
<TableCell align="left">{row.o}</TableCell>
<TableCell align="right" className="font-mono">
{row.b}
</TableCell>
<TableCell align="right" className="pl-2 font-mono">
{row.a}
</TableCell>
</TableRow>
);
})}
</tbody>
</Table>
);
};
@@ -0,0 +1,168 @@
import { getValues, StateVariableProposalRiskFactors } from './risk-factors';
import type { components } from '../../../../../types/explorer';
import { render } from '@testing-library/react';
type kvb = components['schemas']['vegaKeyValueBundle'][];
describe('Risk Factors: getValues', () => {
it('returns null if null is passed in', () => {
const res = getValues(null as unknown as kvb);
expect(res).toBeNull();
});
it('returns a blank template if kvb is empty', () => {
const res = getValues([]);
expect(res).not.toBeNull();
expect(res?.bid.offsetTolerance).toEqual('-');
expect(res?.bid.probabilityTolerance).toEqual('-');
expect(res?.bid.probability).toEqual([]);
expect(res?.bid.offset).toEqual([]);
expect(res?.bid.rows.length).toEqual(0);
expect(res?.ask.offsetTolerance).toEqual('-');
expect(res?.ask.probabilityTolerance).toEqual('-');
expect(res?.ask.probability).toEqual([]);
expect(res?.ask.offset).toEqual([]);
expect(res?.ask.rows.length).toEqual(0);
});
it('parses out a correct bid offset and probability', () => {
const k: kvb = [
{
key: 'bidOffset',
value: { vectorVal: { value: ['1'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['2'] } },
},
];
const res = getValues(k);
expect(res?.bid.offset).toEqual(['1']);
expect(res?.bid.probability).toEqual(['2']);
expect(res?.bid.rows).toEqual([['1', '2']]);
});
it('parses out a correct ask offset and probability', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2'] } },
},
];
const res = getValues(k);
expect(res?.ask.offset).toEqual(['1']);
expect(res?.ask.probability).toEqual(['2']);
expect(res?.ask.rows).toEqual([['1', '2']]);
});
it('parses out a correct ask/bid offset and probability', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2'] } },
},
{
key: 'bidOffset',
value: { vectorVal: { value: ['3'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['4'] } },
},
];
const res = getValues(k);
expect(res?.ask.rows).toEqual([['1', '2']]);
expect(res?.bid.rows).toEqual([['3', '4']]);
});
});
describe('Risk Factors: component', () => {
it('renders 3 rows correctly', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1.1', '1.2', '1.3'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2.2', '2.3', '2.4'] } },
},
{
key: 'bidOffset',
value: { vectorVal: { value: ['1.1', '1.2', '1.3'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['4.4', '4.5', '4.6'] } },
},
];
const screen = render(<StateVariableProposalRiskFactors kvb={k} />);
expect(screen.getByText('Mid offset')).toBeInTheDocument();
expect(screen.getByText('Bid probability')).toBeInTheDocument();
expect(screen.getByText('Ask probability')).toBeInTheDocument();
// First row
expect(screen.getByText('1.1')).toBeInTheDocument();
expect(screen.getByText('2.2')).toBeInTheDocument();
expect(screen.getByText('4.4')).toBeInTheDocument();
// Second row
expect(screen.getByText('1.2')).toBeInTheDocument();
expect(screen.getByText('2.3')).toBeInTheDocument();
expect(screen.getByText('4.5')).toBeInTheDocument();
// Third row
expect(screen.getByText('1.3')).toBeInTheDocument();
expect(screen.getByText('2.4')).toBeInTheDocument();
expect(screen.getByText('4.6')).toBeInTheDocument();
});
it('renders uneven row counts correctly', () => {
const k: kvb = [
{
key: 'askOffset',
value: { vectorVal: { value: ['1.1'] } },
},
{
key: 'askProbability',
value: { vectorVal: { value: ['2.2'] } },
},
{
key: 'bidOffset',
value: { vectorVal: { value: ['1.1', '1.2', '1.3'] } },
},
{
key: 'bidProbability',
value: { vectorVal: { value: ['4.4', '4.5', '4.6'] } },
},
];
const screen = render(<StateVariableProposalRiskFactors kvb={k} />);
expect(screen.getByText('Mid offset')).toBeInTheDocument();
expect(screen.getByText('Bid probability')).toBeInTheDocument();
expect(screen.getByText('Ask probability')).toBeInTheDocument();
// First row, as previous test
expect(screen.getByText('1.1')).toBeInTheDocument();
expect(screen.getByText('2.2')).toBeInTheDocument();
expect(screen.getByText('4.4')).toBeInTheDocument();
// Second row - offset comes from bid, not ask
expect(screen.getByText('1.2')).toBeInTheDocument();
expect(screen.getByText('4.5')).toBeInTheDocument();
// Third row
expect(screen.getByText('1.3')).toBeInTheDocument();
expect(screen.getByText('4.6')).toBeInTheDocument();
// The askOffset levels without a probability render -
expect(screen.getAllByText('-')).toHaveLength(2);
});
});
@@ -0,0 +1,52 @@
import { t } from '@vegaprotocol/react-helpers';
import type { components } from '../../../../../types/explorer';
import { Table, TableRow, TableHeader, TableCell } from '../../../table';
import { getValues } from './bound-factors';
interface StateVariableProposalBoundFactorsProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* State Variable proposals updating Bound Factors. This contains two bundles,
* an up vector and a down vector
*
* This is nearly identical to risk factors.
*/
export const StateVariableProposalBoundFactors = ({
kvb,
}: StateVariableProposalBoundFactorsProps) => {
const v = getValues(kvb);
return (
<Table allowWrap={true} className="w-1/3">
<thead>
<TableRow modifier="bordered">
<TableHeader align="left">{t('Parameter')}</TableHeader>
<TableHeader align="center">{t('New value')}</TableHeader>
<TableHeader align="right">{t('Tolerance')}</TableHeader>
</TableRow>
</thead>
<tbody>
<TableRow modifier="bordered">
<TableCell>{t('Up')}</TableCell>
<TableCell align="right" className="font-mono">
{v.up.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.up.tolerance}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Down')}</TableCell>
<TableCell align="right" className="font-mono">
{v.down.value}
</TableCell>
<TableCell align="right" className="font-mono">
{v.down.tolerance}
</TableCell>
</TableRow>
</tbody>
</Table>
);
};
@@ -0,0 +1,16 @@
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
interface StateVariableProposalUnknownProps {
kvb: readonly components['schemas']['vegaKeyValueBundle'][];
}
/**
* State Variable proposals of an unknown type. Let's just dump
* it out.
*/
export const StateVariableProposalUnknown = ({
kvb,
}: StateVariableProposalUnknownProps) => {
return <SyntaxHighlighter data={kvb} />;
};
@@ -53,7 +53,7 @@ export const TxDetailsBatch = ({
let index = 0;
return (
<div key={`tx-${index}`}>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -32,7 +32,7 @@ export const TxDetailsChainEvent = ({
}
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<ChainEvent txData={txData} />
</TableWithTbody>
@@ -38,7 +38,7 @@ export const TxDetailsDataSubmission = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -35,7 +35,7 @@ export const TxDetailsDelegate = ({
txData.command.delegateSubmission;
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{d.nodeId ? (
<TableRow modifier="bordered">
@@ -21,6 +21,7 @@ import { TxDetailsDataSubmission } from './tx-data-submission';
import { TxProposalVote } from './tx-proposal-vote';
import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -102,6 +103,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsDelegate;
case 'Undelegate':
return TxDetailsUndelegate;
case 'State Variable Proposal':
return TxDetailsStateVariable;
default:
return TxDetailsGeneric;
}
@@ -23,7 +23,7 @@ export const TxDetailsGeneric = ({
}
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
</TableWithTbody>
);
@@ -60,7 +60,7 @@ export const TxDetailsHeartbeat = ({
const blockHeight = txData.command.blockHeight || '';
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Node')}</TableCell>
@@ -36,7 +36,7 @@ export const TxDetailsLiquidityAmendment = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -37,7 +37,7 @@ export const TxDetailsLiquidityCancellation = ({
const marketId: string = cancel.marketId || '-';
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
@@ -35,7 +35,7 @@ export const TxDetailsLiquiditySubmission = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -42,7 +42,7 @@ export const TxDetailsNodeVote = ({
}
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{data && !!data.deposit
? TxDetailsNodeVoteDeposit({ deposit: data })
@@ -29,7 +29,7 @@ export const TxDetailsOrderAmend = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -29,7 +29,7 @@ export const TxDetailsOrderCancel = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -39,7 +39,7 @@ export const TxDetailsOrder = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -32,7 +32,7 @@ export const TxProposalVote = ({
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
@@ -0,0 +1,57 @@
import {
hackyGetMarketFromStateVariable,
hackyGetVariableFromStateVariable,
} from './tx-state-variable-proposal';
describe('Hacky Get market from state variable', () => {
it('Extracts a market id from a known state variable proposal id', () => {
const knownId =
'84fff099818dc4f5319477f0812b4341565cfb32ccf735beede734e386b8108f_5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba_probability_of_trading';
const res = hackyGetMarketFromStateVariable(knownId);
expect(res).toEqual(
'5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba'
);
});
it('Returns null if the string looks a bit like the known one, but with different segments', () => {
const knownId =
'5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba_probability_of_trading';
const res = hackyGetMarketFromStateVariable(knownId);
expect(res).toEqual(null);
});
it('Handles empty/weird data', () => {
expect(hackyGetMarketFromStateVariable(null as unknown as string)).toEqual(
null
);
expect(hackyGetMarketFromStateVariable('')).toEqual(null);
expect(
hackyGetMarketFromStateVariable(undefined as unknown as string)
).toEqual(null);
expect(hackyGetMarketFromStateVariable(2 as unknown as string)).toEqual(
null
);
});
});
describe('Hacky Get Variable from state variable proposal id', () => {
it('Extracts an variable name from a known state variable proposal id', () => {
const knownId =
'84fff099818dc4f5319477f0812b4341565cfb32ccf735beede734e386b8108f_5d69ff4a485a9f963272c8614c1d0d84bb8ea57886f5b11aad53f0ccc77731ba_probability_of_trading';
const res = hackyGetVariableFromStateVariable(knownId);
expect(res).toEqual('probability of trading');
});
it('Handles empty/weird data', () => {
expect(
hackyGetVariableFromStateVariable(null as unknown as string)
).toEqual(null);
expect(hackyGetVariableFromStateVariable('')).toEqual(null);
expect(
hackyGetVariableFromStateVariable(undefined as unknown as string)
).toEqual(null);
expect(hackyGetVariableFromStateVariable(2 as unknown as string)).toEqual(
null
);
});
});
@@ -0,0 +1,113 @@
import { t } from '@vegaprotocol/react-helpers';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { MarketLink } from '../../links';
import type { components } from '../../../../types/explorer';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { StateVariableProposalWrapper } from './state-variable/data-wrapper';
interface TxDetailsStateVariableProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* There is no market ID in the event, but it appears to be encoded in to the variable
* ID so let's pull it out. MarketLink component will handle if it isn't a real market.
*
* Given how liable to break this is, it's wrapped in a try catch
*
* @param stateVarId The full state variable proposal variable name
* @returns null or a string market id
*/
export function hackyGetMarketFromStateVariable(
stateVarId?: string
): string | null {
try {
const res = stateVarId ? stateVarId.split('_')[1] : null;
return res && res.length === 64 ? res : null;
} catch (e) {
return null;
}
}
/**
* There is no event name in the event, but it appears to be encoded in to the variable
* ID so let's pull it out. Will display nothing if it doesn't parse as expected
*
* Given how liable to break this is, it's wrapped in a try catch
*
* @param stateVarId The full state variable proposal variable name
* @returns null or a string variable name
*/
export function hackyGetVariableFromStateVariable(
stateVarId?: string
): string | null {
try {
if (!stateVarId) {
return null;
}
return stateVarId.split('_').slice(2).join(' ').replace('-', ' ');
} catch (e) {
return null;
}
}
/**
* State Variable proposals
*/
export const TxDetailsStateVariable = ({
txData,
pubKey,
blockData,
}: TxDetailsStateVariableProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const command: components['schemas']['v1StateVariableProposal'] =
txData.command.stateVariableProposal;
const variable = hackyGetVariableFromStateVariable(
command.proposal?.stateVarId
);
const marketId = hackyGetMarketFromStateVariable(
command.proposal?.stateVarId
);
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
{marketId ? (
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<MarketLink id={marketId} />
</TableCell>
</TableRow>
) : null}
<TableRow modifier="bordered">
<TableCell>{t('Variable')}</TableCell>
<TableCell className="capitalize">
<span>{variable}</span>
</TableCell>
</TableRow>
</TableWithTbody>
<section>
<StateVariableProposalWrapper
stateVariable={command.proposal?.stateVarId}
kvb={command.proposal?.kvb}
/>
</section>
</>
);
};
@@ -46,7 +46,7 @@ export const TxDetailsUndelegate = ({
txData.command.undelegateSubmission;
return (
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{u.nodeId ? (
<TableRow modifier="bordered">
@@ -41,7 +41,7 @@ export const TxDetailsWithdrawSubmission = ({
return (
<>
<TableWithTbody className="mb-8">
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
@@ -0,0 +1,4 @@
/**
* Equivalent of tailwind's `md` modifier
*/
export const BREAKPOINT_MD = 768;
+134
View File
@@ -0,0 +1,134 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetDocument } from '@vegaprotocol/assets';
import { AssetsDocument } from '@vegaprotocol/assets';
import { AssetStatus } from '@vegaprotocol/types';
const A1: AssetFieldsFragment = {
__typename: 'Asset',
id: '123',
name: 'A ONE',
symbol: 'A1',
decimals: 0,
quantum: '',
status: AssetStatus.STATUS_ENABLED,
source: {
__typename: 'BuiltinAsset',
maxFaucetAmountMint: '',
},
infrastructureFeeAccount: {
__typename: 'AccountBalance',
balance: '',
},
globalRewardPoolAccount: {
__typename: 'AccountBalance',
balance: '',
},
lpFeeRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
makerFeeRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
marketProposerRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
takerFeeRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
};
const A2: AssetFieldsFragment = {
__typename: 'Asset',
id: '456',
name: 'A TWO',
symbol: 'A2',
decimals: 0,
quantum: '',
status: AssetStatus.STATUS_ENABLED,
source: {
__typename: 'BuiltinAsset',
maxFaucetAmountMint: '',
},
infrastructureFeeAccount: {
__typename: 'AccountBalance',
balance: '',
},
globalRewardPoolAccount: {
__typename: 'AccountBalance',
balance: '',
},
lpFeeRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
makerFeeRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
marketProposerRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
takerFeeRewardAccount: {
__typename: 'AccountBalance',
balance: '',
},
};
export const assetsList = [A1, A2];
export const mockAssetsList = {
request: {
query: AssetsDocument,
},
result: {
data: {
assetsConnection: {
__typename: 'AssetsConnection',
edges: [
{
__typename: 'AssetEdge',
node: A1,
},
{
__typename: 'AssetEdge',
node: A2,
},
],
},
},
},
};
export const mockEmptyAssetsList = {
request: {
query: AssetsDocument,
},
result: { data: null },
};
export const mockAssetA1 = {
request: {
query: AssetDocument,
variables: {
assetId: '123',
},
},
result: {
data: {
assetsConnection: {
__typename: 'AssetsConnection',
edges: [
{
__typename: 'AssetEdge',
node: A1,
},
],
},
},
},
};
@@ -1,32 +0,0 @@
fragment AssetsFields on Asset {
id
name
symbol
decimals
source {
... on ERC20 {
contractAddress
}
... on BuiltinAsset {
maxFaucetAmountMint
}
}
infrastructureFeeAccount {
type
balance
market {
id
}
}
}
query ExplorerAssets {
assetsConnection {
edges {
node {
...AssetsFields
}
}
}
}
@@ -1,73 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type AssetsFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string } | null } | null };
export type ExplorerAssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerAssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string } | null } | null } } | null> | null } | null };
export const AssetsFieldsFragmentDoc = gql`
fragment AssetsFields on Asset {
id
name
symbol
decimals
source {
... on ERC20 {
contractAddress
}
... on BuiltinAsset {
maxFaucetAmountMint
}
}
infrastructureFeeAccount {
type
balance
market {
id
}
}
}
`;
export const ExplorerAssetsDocument = gql`
query ExplorerAssets {
assetsConnection {
edges {
node {
...AssetsFields
}
}
}
}
${AssetsFieldsFragmentDoc}`;
/**
* __useExplorerAssetsQuery__
*
* To run a query within a React component, call `useExplorerAssetsQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerAssetsQuery({
* variables: {
* },
* });
*/
export function useExplorerAssetsQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>(ExplorerAssetsDocument, options);
}
export function useExplorerAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>(ExplorerAssetsDocument, options);
}
export type ExplorerAssetsQueryHookResult = ReturnType<typeof useExplorerAssetsQuery>;
export type ExplorerAssetsLazyQueryHookResult = ReturnType<typeof useExplorerAssetsLazyQuery>;
export type ExplorerAssetsQueryResult = Apollo.QueryResult<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>;
@@ -0,0 +1,30 @@
import { t } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../components/route-title';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { AssetsTable } from '../../components/assets/assets-table';
export const Assets = () => {
useDocumentTitle(['Assets']);
useScrollToLocation();
const { data, loading, error } = useAssetsDataProvider();
return (
<section>
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
<AsyncRenderer
noDataMessage={t('This chain has no assets')}
data={data}
loading={loading}
error={error}
>
<div className="h-full relative">
<AssetsTable data={data} />
</div>
</AsyncRenderer>
</section>
);
};
@@ -1,44 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import Assets from './index';
import type { MockedResponse } from '@apollo/client/testing';
import { ExplorerAssetDocument } from '../../components/links/asset-link/__generated__/Asset';
function renderComponent(mock: MockedResponse[]) {
return (
<MemoryRouter>
<MockedProvider mocks={mock}>
<Assets />
</MockedProvider>
</MemoryRouter>
);
}
describe('Assets index', () => {
it('Renders loader when loading', async () => {
const mock = {
request: {
query: ExplorerAssetDocument,
},
result: {
data: {},
},
};
const res = render(renderComponent([mock]));
expect(await res.findByTestId('loader')).toBeInTheDocument();
});
it('Renders EmptyList when loading completes and there are no results', async () => {
const mock = {
request: {
query: ExplorerAssetDocument,
},
result: {
data: {},
},
};
const res = render(renderComponent([mock]));
expect(await res.findByTestId('emptylist')).toBeInTheDocument();
});
});
+1 -53
View File
@@ -1,53 +1 @@
import { getNodes, t } from '@vegaprotocol/react-helpers';
import React from 'react';
import { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading';
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { useExplorerAssetsQuery } from './__generated__/Assets';
import type { AssetsFieldsFragment } from './__generated__/Assets';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import EmptyList from '../../components/empty-list/empty-list';
const Assets = () => {
const { data, loading } = useExplorerAssetsQuery();
useDocumentTitle(['Assets']);
useScrollToLocation();
const assets = getNodes<AssetsFieldsFragment>(data?.assetsConnection);
if (!assets || assets.length === 0) {
if (!loading) {
return (
<section>
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
<EmptyList
heading={t('This chain has no assets')}
label={t('0 assets')}
/>
</section>
);
} else {
return <Loader />;
}
}
return (
<section>
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
{assets.map((a) => {
return (
<React.Fragment key={a.id}>
<SubHeading data-testid="asset-header" id={a.id}>
{a.name} ({a.symbol})
</SubHeading>
<SyntaxHighlighter data={a} />
</React.Fragment>
);
})}
</section>
);
};
export default Assets;
export * from './assets';
@@ -71,7 +71,7 @@ export const PartyAccounts = ({ accounts }: PartyAccountsProps) => {
/>
</td>
<td className="text-md">
<AssetLink id={account.asset.id} />
<AssetLink assetId={account.asset.id} />
</td>
</TableRow>
);
@@ -1,4 +1,4 @@
import Assets from './assets';
import { Assets } from './assets';
import BlockPage from './blocks';
import Governance from './governance';
import Home from './home';
+1 -9
View File
@@ -1,9 +1,6 @@
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 = {
@@ -14,12 +11,7 @@ module.exports = {
],
darkMode: 'class',
theme: {
extend: {
...theme,
colors: {
vega: VegaColours,
},
},
extend: theme,
},
plugins: [vegaCustomClasses],
};
+59 -26
View File
@@ -115,7 +115,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "86666.297",
"total_removed": "0",
"locked_amount": "70875.3107442288190769439",
"locked_amount": "70697.358476038783010308",
"deposits": [
{
"amount": "86666.297",
@@ -181,7 +181,7 @@
"tranche_end": "2023-06-01T00:00:00.000Z",
"total_added": "2500",
"total_removed": "0",
"locked_amount": "1531.52981277981275",
"locked_amount": "1521.2350872507125",
"deposits": [
{
"amount": "2500",
@@ -569,7 +569,7 @@
"tranche_end": "2023-08-01T00:00:00.000Z",
"total_added": "37500",
"total_removed": "328.1417856",
"locked_amount": "35737.9910988336375",
"locked_amount": "35582.71706184775875",
"deposits": [
{
"amount": "7500",
@@ -646,7 +646,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "70810.64594104826369394",
"locked_amount": "70632.856031912127156",
"deposits": [
{
"amount": "129999.45",
@@ -712,7 +712,7 @@
"tranche_end": "2023-09-03T00:00:00.000Z",
"total_added": "62600",
"total_removed": "0",
"locked_amount": "35243.8635210553031",
"locked_amount": "35115.32668061897062",
"deposits": [
{
"amount": "10000",
@@ -905,7 +905,7 @@
"tranche_end": "2023-09-17T00:00:00.000Z",
"total_added": "5000",
"total_removed": "0",
"locked_amount": "3006.7858954845255",
"locked_amount": "2996.519374682902",
"deposits": [
{
"amount": "5000",
@@ -1116,7 +1116,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "97499.58",
"total_removed": "0",
"locked_amount": "12680.8489810395158446602",
"locked_amount": "12506.4536420561534866824",
"deposits": [
{
"amount": "97499.58",
@@ -1149,7 +1149,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "135173.4239508",
"total_removed": "98230.390980249184455396",
"locked_amount": "17332.53129983515100413698276",
"locked_amount": "17094.162979544133231208218516",
"deposits": [
{
"amount": "135173.4239508",
@@ -1195,7 +1195,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "32499.86",
"total_removed": "0",
"locked_amount": "5334.6141797746525149112",
"locked_amount": "5261.2490723107703745224",
"deposits": [
{
"amount": "32499.86",
@@ -1228,7 +1228,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "10833.29",
"total_removed": "0",
"locked_amount": "1736.3651496459689709353",
"locked_amount": "1712.4855190846272169196",
"deposits": [
{
"amount": "10833.29",
@@ -1261,7 +1261,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "22749.93",
"total_removed": "0",
"locked_amount": "6490.920739528795672116",
"locked_amount": "6401.6533470713355214011",
"deposits": [
{
"amount": "6500",
@@ -1400,7 +1400,7 @@
"tranche_end": "2023-05-01T00:00:00.000Z",
"total_added": "22500",
"total_removed": "4140.290726625",
"locked_amount": "10006.330570902395325",
"locked_amount": "9913.1661487108665",
"deposits": [
{
"amount": "7500",
@@ -1609,7 +1609,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "928642.9598472029154",
"locked_amount": "597898.524931760479177335",
"locked_amount": "593915.2619183745624309166",
"deposits": [
{
"amount": "1852091.69",
@@ -33603,7 +33603,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "592998.0503546212334",
"locked_amount": "943263.19139257341189174758",
"locked_amount": "937142.30296588429147888632",
"deposits": [
{
"amount": "1998.95815",
@@ -34928,8 +34928,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15870102.715470999700000001",
"total_removed": "561830.98711463838793952",
"locked_amount": "8644438.2990334618538022518075508209678292",
"total_removed": "563236.46299044578477952",
"locked_amount": "8622734.02167102114525126607450796049608",
"deposits": [
{
"amount": "16249.93",
@@ -35518,6 +35518,16 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x61b0366935b55d5f021bbf4c0e1c7595e1a8628fe24c135e62e66d82dc9cc021"
},
{
"amount": "429.5754139050275",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x2dc4f71aa0fc8695025203f3ecb4f64d0728c6b38f83796442443fdc3edb991a"
},
{
"amount": "975.90046190236934",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
"tx": "0xb39a9645c363292992430b1ac8bd16052b46a7eb1624e7e8ba86a0d1663585b2"
},
{
"amount": "858.360074993579125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -37149,6 +37159,12 @@
"tranche_id": 2,
"tx": "0x61b0366935b55d5f021bbf4c0e1c7595e1a8628fe24c135e62e66d82dc9cc021"
},
{
"amount": "429.5754139050275",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0x2dc4f71aa0fc8695025203f3ecb4f64d0728c6b38f83796442443fdc3edb991a"
},
{
"amount": "858.360074993579125",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -38237,8 +38253,8 @@
}
],
"total_tokens": "259998.8875",
"withdrawn_tokens": "118099.626035107129875",
"remaining_tokens": "141899.261464892870125"
"withdrawn_tokens": "118529.201449012157375",
"remaining_tokens": "141469.686050987842625"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -38483,6 +38499,12 @@
"tranche_id": 2,
"tx": "0xb0d4e11c4f5aab1c4c65994c14d5272ecb4df9972ddee36ae5389de3731a3e35"
},
{
"amount": "975.90046190236934",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
"tranche_id": 2,
"tx": "0xb39a9645c363292992430b1ac8bd16052b46a7eb1624e7e8ba86a0d1663585b2"
},
{
"amount": "1293.67099136315494",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
@@ -38683,8 +38705,8 @@
}
],
"total_tokens": "150551.801",
"withdrawn_tokens": "67672.37507088538159",
"remaining_tokens": "82879.42592911461841"
"withdrawn_tokens": "68648.27553278775093",
"remaining_tokens": "81903.52546721224907"
},
{
"address": "0x4d982Ab0823fD2f48e934a7be2bb0a5374a26148",
@@ -40439,8 +40461,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "3922560.622544070958832032",
"locked_amount": "2259045.016117707539647181202605103",
"total_removed": "3923097.864384523185862282",
"locked_amount": "2239007.76583982230552610140543878",
"deposits": [
{
"amount": "129284.449",
@@ -40709,6 +40731,11 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xe2b3e0f908fa79636a02b919ede475f86857f0588badd6c85249eb97802fc331"
},
{
"amount": "537.24184045222703025",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x58aa11462e034da8d46955c61b513f80de89ebcfd89a96860afc3f8da5bfa73e"
},
{
"amount": "8950.14985089483210984",
"user": "0x66827bCD635f2bB1779d68c46aEB16541bCA6ba8",
@@ -43558,6 +43585,12 @@
"tranche_id": 3,
"tx": "0xe2b3e0f908fa79636a02b919ede475f86857f0588badd6c85249eb97802fc331"
},
{
"amount": "537.24184045222703025",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x58aa11462e034da8d46955c61b513f80de89ebcfd89a96860afc3f8da5bfa73e"
},
{
"amount": "1192.05386354121365675",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -45948,8 +45981,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "303162.4297735222582315",
"remaining_tokens": "55961.0398014777417685"
"withdrawn_tokens": "303699.67161397448526175",
"remaining_tokens": "55423.79796102551473825"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -47287,7 +47320,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "5778205.3912159303",
"total_removed": "2730068.739915456784546642",
"locked_amount": "575658.944918433408911164999611974",
"locked_amount": "567742.106149360000910747830518459",
"deposits": [
{
"amount": "552496.6455",
@@ -49270,7 +49303,7 @@
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "32520.5457984892685",
"locked_amount": "149465.44459842708862109310096396",
"locked_amount": "148495.55483872828913703876509388",
"deposits": [
{
"amount": "3000",
+3
View File
@@ -2,10 +2,12 @@ import {
RestConnector,
JsonRpcConnector,
ViewConnector,
BrowserConnector,
} from '@vegaprotocol/wallet';
export const rest = new RestConnector();
export const jsonRpc = new JsonRpcConnector();
export const browser = new BrowserConnector();
let view: ViewConnector;
if (typeof window !== 'undefined') {
@@ -19,4 +21,5 @@ export const Connectors = {
rest,
jsonRpc,
view,
browser,
};
+1 -1
View File
@@ -6,7 +6,7 @@ import { AssetDocument } from './__generated__/Asset';
export type Asset = AssetFieldsFragment;
const getData = (responseData: AssetQuery | null) => {
export const getData = (responseData: AssetQuery | null | undefined) => {
const foundAssets = responseData?.assetsConnection?.edges
?.filter((e) => Boolean(e?.node))
.map((e) => e?.node as Asset);
+21 -4
View File
@@ -1,6 +1,12 @@
import { t } from '@vegaprotocol/react-helpers';
import { useAssetsDataProvider } from './assets-data-provider';
import { Button, Dialog, Icon, Splash } from '@vegaprotocol/ui-toolkit';
import {
Button,
Dialog,
Icon,
Splash,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
import { create } from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
import { AssetProposalNotification } from '@vegaprotocol/governance';
@@ -9,8 +15,9 @@ export type AssetDetailsDialogStore = {
isOpen: boolean;
id: string;
trigger: HTMLElement | null | undefined;
asJson: boolean;
setOpen: (isOpen: boolean) => void;
open: (id: string, trigger?: HTMLElement | null) => void;
open: (id: string, trigger?: HTMLElement | null, asJson?: boolean) => void;
};
export const useAssetDetailsDialogStore = create<AssetDetailsDialogStore>(
@@ -18,14 +25,16 @@ export const useAssetDetailsDialogStore = create<AssetDetailsDialogStore>(
isOpen: false,
id: '',
trigger: null,
asJson: false,
setOpen: (isOpen) => {
set({ isOpen: isOpen });
},
open: (id, trigger?) => {
open: (id, trigger?, asJson = false) => {
set({
isOpen: true,
id,
trigger,
asJson,
});
},
})
@@ -36,6 +45,7 @@ export interface AssetDetailsDialogProps {
trigger?: HTMLElement | null;
open: boolean;
onChange: (open: boolean) => void;
asJson?: boolean;
}
export const AssetDetailsDialog = ({
@@ -43,6 +53,7 @@ export const AssetDetailsDialog = ({
trigger,
open,
onChange,
asJson = false,
}: AssetDetailsDialogProps) => {
const { data } = useAssetsDataProvider();
@@ -51,7 +62,13 @@ export const AssetDetailsDialog = ({
const content = asset ? (
<div className="my-2">
<AssetProposalNotification assetId={asset.id} />
<AssetDetailsTable asset={asset} />
{asJson ? (
<div className="pr-8">
<SyntaxHighlighter size="smaller" data={asset} />
</div>
) : (
<AssetDetailsTable asset={asset} />
)}
</div>
) : (
<div className="py-12" data-testid="splash">
+14 -4
View File
@@ -2,6 +2,7 @@ import { useEtherscanLink } from '@vegaprotocol/environment';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import type * as Schema from '@vegaprotocol/types';
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, Icon } from '@vegaprotocol/ui-toolkit';
import { Link } from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
@@ -105,7 +106,16 @@ export const rows: Rows = [
return;
}
return <ContractAddressLink address={asset.source.contractAddress} />;
return (
<>
<ContractAddressLink address={asset.source.contractAddress} />{' '}
<CopyWithTooltip text={asset.source.contractAddress}>
<button title={t('Copy address to clipboard')}>
<Icon size={3} name="duplicate" />
</button>
</CopyWithTooltip>
</>
);
},
},
{
@@ -180,7 +190,7 @@ export const rows: Rows = [
},
];
const AssetStatusMapping: Mapping = {
export const AssetStatusMapping: Mapping = {
STATUS_ENABLED: {
value: t('Enabled'),
tooltip: t('Asset can be used on the Vega network'),
@@ -199,7 +209,7 @@ const AssetStatusMapping: Mapping = {
},
};
const AssetTypeMapping: Mapping = {
export const AssetTypeMapping: Mapping = {
BuiltinAsset: {
value: 'Builtin asset',
tooltip: t('A Vega builtin asset'),
@@ -274,7 +284,7 @@ const ContractAddressLink = ({ address }: { address: string }) => {
const etherscanLink = useEtherscanLink();
const href = etherscanLink(`/address/${address}`);
return (
<Link href={href} target="_blank">
<Link href={href} target="_blank" title={t('View on etherscan')}>
{address}
</Link>
);
+2 -2
View File
@@ -5,7 +5,7 @@ import { addMockWalletCommand } from './lib/mock-rest';
import { addMockWeb3ProviderCommand } from './lib/commands/mock-web3-provider';
import { addSlackCommand } from './lib/commands/slack';
import { addHighlightLog } from './lib/commands/highlight-log';
import { addGetAssetInformation } from './lib/commands/get-asset-information';
import { addGetAssets } from './lib/commands/get-assets';
import { addVegaWalletReceiveFaucetedAsset } from './lib/commands/vega-wallet-receive-fauceted-asset';
import { addContainsExactly } from './lib/commands/contains-exactly';
import { addGetNetworkParameters } from './lib/commands/get-network-parameters';
@@ -26,7 +26,7 @@ addMockWalletCommand();
addMockWeb3ProviderCommand();
addHighlightLog();
addVegaWalletReceiveFaucetedAsset();
addGetAssetInformation();
addGetAssets();
addContainsExactly();
addGetNetworkParameters();
addUpdateCapsuleMultiSig();
@@ -1,38 +0,0 @@
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
get_asset_information(): void;
}
}
}
export function addGetAssetInformation() {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
Cypress.Commands.add('get_asset_information', () => {
const mutation =
'{ assetsConnection{edges{node{name id symbol decimals source{__typename \
... on ERC20{contractAddress} \
... on BuiltinAsset{maxFaucetAmountMint}} \
infrastructureFeeAccount{__typename type balance} \
globalRewardPoolAccount {balance}}}}}';
cy.request({
method: 'POST',
url: `http://localhost:3028/query`,
body: {
query: mutation,
},
headers: { 'content-type': 'application/json' },
})
.its('body.data.assetsConnection.edges')
.then(function (response) {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
const object = response.reduce(function (assets, entry) {
assets[entry.node.name] = entry;
return assets;
}, {});
return object;
});
});
}
@@ -0,0 +1,83 @@
import { gql } from '@apollo/client';
import { print } from 'graphql';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
getAssets(): Chainable<Record<string, AssetFieldsFragment>>;
}
}
}
export function addGetAssets() {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
Cypress.Commands.add('getAssets', () => {
// TODO: Investigate why importing here an actual AssetsDocument fails and
// causes cypress's webpack to go bonkers
const query = gql`
query Assets {
assetsConnection {
edges {
node {
id
name
symbol
decimals
quantum
source {
__typename
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
... on BuiltinAsset {
maxFaucetAmountMint
}
}
status
infrastructureFeeAccount {
balance
}
globalRewardPoolAccount {
balance
}
takerFeeRewardAccount {
balance
}
makerFeeRewardAccount {
balance
}
lpFeeRewardAccount {
balance
}
marketProposerRewardAccount {
balance
}
}
}
}
}
`;
cy.request({
method: 'POST',
url: 'http://localhost:3028/query',
body: {
query: print(query),
},
headers: { 'content-type': 'application/json' },
})
.its('body.data.assetsConnection.edges')
.then((edges) => {
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
return edges.reduce((list, edge) => {
list[edge.node.name] = edge.node;
return list;
}, {});
});
});
}
@@ -21,7 +21,7 @@ export function addVegaWalletReceiveFaucetedAsset() {
`Topping up vega wallet with ${assetName}, amount: ${amount}`
);
// @ts-ignore - ignoring Cypress type error which gets resolved when Cypress uses the command
cy.get_asset_information().then((assets) => {
cy.getAssets().then((assets) => {
console.log(assets);
const asset = assets[assetName];
if (assets[assetName] !== undefined) {
@@ -15,7 +15,7 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
},
'.syntax-highlighter-wrapper .hljs': {
fontSize: '1rem',
fontFamily: "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace",
fontFamily: "'Roboto Mono', monospace",
display: 'block',
overflowX: 'auto',
padding: '1em',
@@ -23,6 +23,10 @@ const vegaCustomClasses = plugin(function ({ addUtilities }) {
color: colors.neutral[700],
border: `1px solid ${colors.neutral[300]}`,
},
'.syntax-highlighter-wrapper-sm .hljs': {
fontSize: '0.875rem',
lineHeight: '1.25rem',
},
'.dark .syntax-highlighter-wrapper .hljs': {
background: colors.neutral[800],
color: theme.colors.vega.green.DEFAULT,
@@ -1,8 +1,19 @@
import classNames from 'classnames';
import Highlighter from 'react-syntax-highlighter';
export const SyntaxHighlighter = ({ data }: { data: unknown }) => {
export const SyntaxHighlighter = ({
data,
size = 'default',
}: {
data: unknown;
size?: 'smaller' | 'default';
}) => {
return (
<div className="syntax-highlighter-wrapper">
<div
className={classNames('syntax-highlighter-wrapper', {
'syntax-highlighter-wrapper-sm': size === 'smaller',
})}
>
<Highlighter language="json" useInlineStyles={false}>
{JSON.stringify(data, null, ' ')}
</Highlighter>
@@ -0,0 +1,213 @@
import capitalize from 'lodash/capitalize';
import { createDocsLinks, t } from '@vegaprotocol/react-helpers';
import {
ButtonLink,
Diamond,
Link,
Loader,
Tick,
} from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { WalletClientError } from '@vegaprotocol/wallet-client';
import type { BrowserConnector } from '../connectors';
import { BrowserClientErrors } from '../connectors';
import { ConnectDialogTitle } from './connect-dialog-elements';
import { Status } from '../use-json-rpc-connect';
import { useEnvironment } from '@vegaprotocol/environment';
export const ServiceErrors = {
NO_HEALTHY_NODE: 1000,
REQUEST_PROCESSING: -32000,
};
export const BrowserConnectorForm = ({
connector,
appChainId,
status,
error,
reset,
}: {
connector: BrowserConnector;
appChainId: string;
status: Status;
error: WalletClientError | null;
onConnect: () => void;
reset: () => void;
}) => {
if (status === Status.Idle) {
return null;
}
return (
<Connecting
status={status}
error={error}
connector={connector}
appChainId={appChainId}
reset={reset}
/>
);
};
const Connecting = ({
status,
error,
connector,
appChainId,
reset,
}: {
status: Status;
error: WalletClientError | null;
connector: BrowserConnector;
appChainId: string;
reset: () => void;
}) => {
if (status === Status.Error) {
return <Error error={error} appChainId={appChainId} onTryAgain={reset} />;
}
if (status === Status.CheckingVersion) {
return (
<>
<ConnectDialogTitle>{t('Checking wallet version')}</ConnectDialogTitle>
<Center>
<Loader />
</Center>
<p className="text-center">
{t('Checking your wallet is compatible with this app')}
</p>
</>
);
}
if (status === Status.GettingChainId) {
return (
<>
<ConnectDialogTitle>{t('Verifying chain')}</ConnectDialogTitle>
<Center>
<Loader />
</Center>
</>
);
}
if (status === Status.Connected) {
return (
<>
<ConnectDialogTitle>{t('Successfully connected')}</ConnectDialogTitle>
<Center>
<Tick />
</Center>
</>
);
}
if (status === Status.Connecting || status === Status.GettingPerms) {
return (
<>
<ConnectDialogTitle>{t('Connecting...')}</ConnectDialogTitle>
<Center>
<Diamond />
</Center>
<p className="text-center">
{t(
"Approve the connection from your Browser wallet app. If you have multiple wallets you'll need to choose which to connect with."
)}
</p>
</>
);
}
return null;
};
const Center = ({ children }: { children: ReactNode }) => {
return (
<div className="flex justify-center items-center my-6">{children}</div>
);
};
const Error = ({
error,
appChainId,
onTryAgain,
}: {
error: WalletClientError | null;
appChainId: string;
onTryAgain: () => void;
}) => {
let title = t('Something went wrong');
let text: ReactNode | undefined = t('An unknown error occurred');
let tryAgain: ReactNode | null = (
<p className="text-center">
<ButtonLink onClick={onTryAgain}>{t('Try again')}</ButtonLink>
</p>
);
const { VEGA_DOCS_URL } = useEnvironment();
if (error) {
if (error.code === BrowserClientErrors.NO_SERVICE.code) {
title = t('No wallet detected');
text = t(
'No Vega Wallet application running. Please install the vega wallet browser extension.'
);
} else if (error.code === BrowserClientErrors.WRONG_NETWORK.code) {
title = t('Wrong network');
text = t(
'To complete your wallet connection, set your wallet network in your app to "%s".',
appChainId
);
} else if (error.code === ServiceErrors.NO_HEALTHY_NODE) {
title = error.title;
text = (
<>
{capitalize(error.message)}
{'. '}
{VEGA_DOCS_URL && (
<Link
href={createDocsLinks(VEGA_DOCS_URL).VEGA_WALLET_CONCEPTS_URL}
>
{t('Read the docs to troubleshoot')}
</Link>
)}
</>
);
} else if (error.code === ServiceErrors.REQUEST_PROCESSING) {
title = t('Connection in progress');
text = t('Approve the connection from your Vega wallet app.');
tryAgain = null;
} else if (error.code === 0) {
title = t('Wrong network');
text = (
<>
{t(
`To complete your wallet connection, set your wallet network in your
app to %s.`,
appChainId
)}
</>
);
} else if (error.code === BrowserClientErrors.INVALID_WALLET.code) {
title = error.title;
const errorData = error.message?.split('\n ') || [];
text = (
<span className="flex flex-col">
{errorData.map((str, i) => (
<span key={i}>{str}</span>
))}
</span>
);
} else {
title = t(error.title);
text = t(error.message);
}
}
return (
<>
<ConnectDialogTitle>{title}</ConnectDialogTitle>
<p className="text-center mb-2 first-letter:uppercase">{text}</p>
{tryAgain}
</>
);
};
@@ -13,9 +13,14 @@ import type { WalletClientError } from '@vegaprotocol/wallet-client';
import { ExternalLinks, t, useChainIdQuery } from '@vegaprotocol/react-helpers';
import type { VegaConnector } from '../connectors';
import { ViewConnector } from '../connectors';
import { JsonRpcConnector, RestConnector } from '../connectors';
import {
JsonRpcConnector,
RestConnector,
BrowserConnector,
} from '../connectors';
import { RestConnectorForm } from './rest-connector-form';
import { JsonRpcConnectorForm } from './json-rpc-connector-form';
import { BrowserConnectorForm } from './browser-connector-form';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import {
ConnectDialogContent,
@@ -24,11 +29,12 @@ import {
} from './connect-dialog-elements';
import type { Status } from '../use-json-rpc-connect';
import { useJsonRpcConnect } from '../use-json-rpc-connect';
import { useBrowserConnect } from '../use-browser-connect';
import { ViewConnectorForm } from './view-connector-form';
export const CLOSE_DELAY = 1700;
type Connectors = { [key: string]: VegaConnector };
type WalletType = 'jsonRpc' | 'hosted' | 'view';
type WalletType = 'jsonRpc' | 'hosted' | 'view' | 'browser';
export interface VegaConnectDialogProps {
connectors: Connectors;
@@ -117,7 +123,7 @@ export const VegaConnectDialog = ({
size="small"
onChange={updateVegaWalletDialog}
>
{renderContent()}
<div id="wallet-dialog-000">{renderContent()}</div>
</Dialog>
);
};
@@ -148,6 +154,8 @@ const ConnectDialogContainer = ({
}, [closeDialog]);
const { connect, ...jsonRpcState } = useJsonRpcConnect(delayedOnConnect);
const { connect: connectBW, ...browserState } =
useBrowserConnect(delayedOnConnect);
const handleSelect = (type: WalletType, isHosted = false) => {
let connector;
@@ -174,6 +182,10 @@ const ConnectDialogContainer = ({
if (connector instanceof JsonRpcConnector) {
connect(connector, appChainId);
}
if (connector instanceof BrowserConnector) {
connectBW(connector, appChainId);
}
};
return selectedConnector !== undefined && walletType !== undefined ? (
@@ -181,6 +193,7 @@ const ConnectDialogContainer = ({
type={walletType}
connector={selectedConnector}
jsonRpcState={jsonRpcState}
browserState={browserState}
onConnect={closeDialog}
appChainId={appChainId}
reset={reset}
@@ -212,6 +225,13 @@ const ConnectorList = ({
<ConnectDialogTitle>{t('Connect')}</ConnectDialogTitle>
<CustomUrlInput walletUrl={walletUrl} setWalletUrl={setWalletUrl} />
<ul data-testid="connectors-list" className="mb-6">
<li className="mb-4 last:mb-0">
<ConnectionOption
type="browser"
text={t('Connect Browser wallet')}
onClick={() => onSelect('browser')}
/>
</li>
<li className="mb-4 last:mb-0">
<ConnectionOption
type="jsonRpc"
@@ -247,6 +267,7 @@ const SelectedForm = ({
connector,
appChainId,
jsonRpcState,
browserState,
reset,
onConnect,
}: {
@@ -257,6 +278,10 @@ const SelectedForm = ({
status: Status;
error: WalletClientError | null;
};
browserState: {
status: Status;
error: WalletClientError | null;
};
reset: () => void;
onConnect: () => void;
}) => {
@@ -322,6 +347,24 @@ const SelectedForm = ({
);
}
if (connector instanceof BrowserConnector) {
return (
<>
<ConnectDialogContent>
<BrowserConnectorForm
connector={connector}
status={browserState.status}
error={browserState.error}
onConnect={onConnect}
appChainId={appChainId}
reset={reset}
/>
</ConnectDialogContent>
<ConnectDialogFooter />
</>
);
}
throw new Error('No connector selected');
};
@@ -334,6 +377,7 @@ const ConnectionOption = ({
text: string;
onClick: () => void;
}) => {
const id = type === 'browser' ? 'vega-wallet-connect' : undefined;
return (
<Button
onClick={onClick}
@@ -341,6 +385,7 @@ const ConnectionOption = ({
fill={true}
variant={['hosted', 'view'].includes(type) ? 'default' : 'primary'}
data-testid={`connector-${type}`}
id={id}
>
<span className="-mx-6 flex text-left justify-between items-center">
{text}
@@ -0,0 +1,138 @@
import { t } from '@vegaprotocol/react-helpers';
import type { WalletClientError } from '@vegaprotocol/wallet-client';
import { WalletClient } from '@vegaprotocol/wallet-client';
import { clearConfig, getConfig, setConfig } from '../storage';
import type { Transaction, VegaConnector } from './vega-connector';
import { WalletError } from './vega-connector';
export const BrowserClientErrors = {
NO_SERVICE: new WalletError(t('No service'), 100),
INVALID_WALLET: new WalletError(t('Wallet version invalid'), 103),
WRONG_NETWORK: new WalletError(
t('Wrong network'),
104,
t('App is configured to work with a different chain')
),
UNKNOWN: new WalletError(
t('Something went wrong'),
105,
t('Unknown error occurred')
),
NO_CLIENT: new WalletError(t('No client found.'), 106),
} as const;
export class BrowserConnector implements VegaConnector {
url: null;
reqId = 0;
client?: WalletClient;
constructor() {
this.url = null;
const cfg = getConfig();
if (cfg && cfg.connector === 'browser') {
this.initialize();
this.connectWallet();
}
}
initialize() {
if (!this.client) {
try {
this.client = new WalletClient({
type: 'browser',
firefoxId: '62739efdd16a15a5ff4527bcd08fe6302b18b752@temporary-addon',
chromeId: 'pebgkinfegfpnkamklihgpeolleghngl',
});
} catch (err) {
throw BrowserClientErrors.NO_SERVICE;
}
}
}
async getChainId() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
try {
return await this.client.GetChainId();
} catch (err) {
const {
code = BrowserClientErrors.UNKNOWN.code,
message = BrowserClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
async connectWallet() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
try {
await this.client.ConnectWallet();
return null;
} catch (err) {
const {
code = BrowserClientErrors.UNKNOWN.code,
message = BrowserClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
// connect actually calling list_keys here, not to be confused with connect_wallet
// which retrieves the session token
async connect() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
try {
const result = await this.client.ListKeys();
return result.keys;
} catch (err) {
const {
code = BrowserClientErrors.UNKNOWN.code,
message = BrowserClientErrors.UNKNOWN.message,
title,
} = err as WalletClientError;
throw new WalletError(title, code, message);
}
}
async disconnect() {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
await this.client.DisconnectWallet();
clearConfig();
}
async sendTx(pubKey: string, transaction: Transaction) {
if (!this.client) {
throw BrowserClientErrors.NO_CLIENT;
}
const result = await this.client.SendTransaction({
publicKey: pubKey,
sendingMode: 'TYPE_SYNC',
transaction,
});
return {
transactionHash: result.transactionHash,
sentAt: result.sentAt,
receivedAt: result.receivedAt,
signature: result.transaction.signature.value,
};
}
async checkCompat() {
return true;
}
}
+1
View File
@@ -3,3 +3,4 @@ export * from './rest-connector';
export * from './injected-connector';
export * from './json-rpc-connector';
export * from './view-connector';
export * from './browser-connector';
@@ -37,6 +37,7 @@ export class JsonRpcConnector implements VegaConnector {
if (cfg && cfg.url) {
this.url = cfg.url;
this.client = new WalletClient({
type: 'http',
address: cfg.url,
token: cfg.token ?? undefined,
onTokenChange: (token) => {
@@ -53,6 +54,7 @@ export class JsonRpcConnector implements VegaConnector {
set url(url: string) {
this._url = url;
this.client = new WalletClient({
type: 'http',
address: url,
token: this.token ?? undefined,
onTokenChange: (token) =>
@@ -71,8 +73,7 @@ export class JsonRpcConnector implements VegaConnector {
throw ClientErrors.NO_CLIENT;
}
try {
const { result } = await this.client.GetChainId();
return result;
return await this.client.GetChainId();
} catch (err) {
const {
code = ClientErrors.UNKNOWN.code,
@@ -109,7 +110,7 @@ export class JsonRpcConnector implements VegaConnector {
}
try {
const { result } = await this.client.ListKeys();
const result = await this.client.ListKeys();
return result.keys;
} catch (err) {
const {
@@ -135,7 +136,7 @@ export class JsonRpcConnector implements VegaConnector {
throw ClientErrors.NO_CLIENT;
}
const { result } = await this.client.SendTransaction({
const result = await this.client.SendTransaction({
publicKey: pubKey,
sendingMode: 'TYPE_SYNC',
transaction,
+1 -1
View File
@@ -2,7 +2,7 @@ import { LocalStorage } from '@vegaprotocol/react-helpers';
interface ConnectorConfig {
token: string | null;
connector: 'rest' | 'jsonRpc' | 'view';
connector: 'rest' | 'jsonRpc' | 'view' | 'browser';
url: string | null;
}
+76
View File
@@ -0,0 +1,76 @@
import { useCallback, useState } from 'react';
import { WalletClientError } from '@vegaprotocol/wallet-client';
import type { BrowserConnector } from './connectors';
import { ClientErrors } from './connectors';
import { useVegaWallet } from './use-vega-wallet';
export enum Status {
Idle = 'Idle',
CheckingVersion = 'CheckingVersion',
GettingChainId = 'GettingChainId',
Connecting = 'Connecting',
GettingPerms = 'GettingPerms',
ListingKeys = 'ListingKeys',
Connected = 'Connected',
Error = 'Error',
}
export const useBrowserConnect = (onConnect: () => void) => {
const { connect } = useVegaWallet();
const [status, setStatus] = useState(Status.Idle);
const [error, setError] = useState<WalletClientError | null>(null);
const attemptConnect = useCallback(
async (connector: BrowserConnector, appChainId: string) => {
try {
connector.initialize();
// Check that the running wallet is compatible with this connector
setStatus(Status.CheckingVersion);
await connector.checkCompat();
// Check if wallet is configured for the same chain as the app
setStatus(Status.GettingChainId);
// Dont throw in when cypress is running as trading app relies on
// mocks which result in a mismatch between chainId for app and
// chainId for wallet
if (!('Cypress' in window)) {
const chainIdResult = await connector.getChainId();
console.log('CHAIN RES: ', chainIdResult);
if (chainIdResult.chainID !== appChainId) {
// Throw wallet error for consitent error handling
throw ClientErrors.WRONG_NETWORK;
}
}
// Start connection flow. User will be prompted to select a wallet and enter
// its password in the wallet application, promise will resolve once successful
// or it will throw
setStatus(Status.Connecting);
await connector.connectWallet();
setStatus(Status.GettingPerms);
// Call connect in the wallet provider. The connector will be stored for
// future actions such as sending transactions
await connect(connector);
setStatus(Status.Connected);
onConnect();
} catch (err) {
console.log('ERROR!!!!!', err);
if (err instanceof WalletClientError) {
setError(err);
}
setStatus(Status.Error);
}
},
[onConnect, connect]
);
return {
status,
error,
connect: attemptConnect,
};
};