feat(explorer): data submission tx view (#2698)
This commit is contained in:
parent
7db2a58f9d
commit
a60379b40f
@ -0,0 +1,45 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { JSONOracleData, parseData } from './data-submission-json-oracle';
|
||||
|
||||
describe('JSONOracleData', () => {
|
||||
it('renders a code block if there is a payload that is base64 encoded', () => {
|
||||
const p = 'eyJwcmljZXMuRVRIOTkudmFsdWUiOiAiMTMwNTAwMDAwMCJ9';
|
||||
const screen = render(<JSONOracleData payload={p} />);
|
||||
expect(screen.getByTestId('json-code')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an error if b64 decoding fails', () => {
|
||||
const p = 'not-encoded';
|
||||
const screen = render(<JSONOracleData payload={p} />);
|
||||
|
||||
expect(
|
||||
screen.getByText('Awaiting Block Explorer transaction details')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse JSON oracle data', () => {
|
||||
it('returns null for an invalid b64 string', () => {
|
||||
expect(parseData('🤷')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a string that looks like an object', () => {
|
||||
expect(parseData('{}')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for null', () => {
|
||||
expect(parseData(null as unknown as string)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns a string for b64 encoded data', () => {
|
||||
const res = parseData('dGVzdCA9IHRydWU=');
|
||||
expect(typeof res).toEqual('string');
|
||||
expect(res).toEqual('test = true');
|
||||
});
|
||||
|
||||
it('returns a string for b64 encoded json, without any extra parsing', () => {
|
||||
const res = parseData('eyJ0ZXN0Ijp0cnVlfQ==');
|
||||
expect(typeof res).toEqual('string');
|
||||
expect(res).toEqual('{"test":true}');
|
||||
});
|
||||
});
|
@ -0,0 +1,40 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface JSONOracleDataProps {
|
||||
payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a JSON oracle. Given the shape could be any valid JSON,
|
||||
* this doesn't try to do anything smart - it just renders the message
|
||||
*/
|
||||
export const JSONOracleData = ({ payload }: JSONOracleDataProps) => {
|
||||
const decodedSubmission = parseData(payload);
|
||||
|
||||
if (!decodedSubmission) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<code data-testid="json-code">{decodedSubmission}</code>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely parses the JSON Oracle payload
|
||||
*
|
||||
* @param payload base64 encoded JSON
|
||||
* @returns Object or null
|
||||
*/
|
||||
export function parseData(payload: string) {
|
||||
try {
|
||||
if (!payload || typeof payload !== 'string') {
|
||||
throw new Error('Not a string');
|
||||
}
|
||||
return atob(payload);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { OpenOracleData, parseData } from './data-submission-open-oracle';
|
||||
|
||||
describe('OpenOracleData', () => {
|
||||
it('renders a table if there is data', () => {
|
||||
const p =
|
||||
'eyJtZXNzYWdlcyI6IFsiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA2M2M3ZTMwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNGYxZjM4ZWEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNjcwNzI2OTYzNjU3MzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAzNDI1NDQzMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCJdLCAicHJpY2VzIjogeyJFVEgiOiIxMjMifSwgInNpZ25hdHVyZXMiOiBbIjB4N2U0Nzc2OWEyNzI5NzIwN2Q5ZTIyMmM3YTY2YjBlYzk3Njg4MWY3NmVkYjg5OGFhYzQ5OTRlZWYwOTc4MmI3NGUxMGE5MTJmZWQ1Y2E3NmFlN2U3NzVjOWZiOTBjZGE4ZjhkMDhlNzUyOTE1NzQ2ZTY2OGE1ZWM3OWRmOTZmNmQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDFjIl0sICJ0aW1lc3RhbXAiOiAiMTIzIn0=';
|
||||
|
||||
const screen = render(<OpenOracleData payload={p} />);
|
||||
expect(screen.getByTestId('decoded-payload')).toBeInTheDocument();
|
||||
expect(screen.getByText('ETH')).toBeInTheDocument();
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an error if decoding fails', () => {
|
||||
const p = 'not-encoded';
|
||||
const screen = render(<OpenOracleData payload={p} />);
|
||||
|
||||
expect(
|
||||
screen.getByText('Awaiting Block Explorer transaction details')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parse Open oracle data', () => {
|
||||
it('returns null for an invalid b64 string', () => {
|
||||
expect(parseData('🤷')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a string that looks like an object', () => {
|
||||
expect(parseData('{}')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for null', () => {
|
||||
expect(parseData(null as unknown as string)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns a string for b64 encoded data', () => {
|
||||
const res = parseData('dGVzdCA9IHRydWU=');
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null if the parsed object does not look like te right shape', () => {
|
||||
// Encoded: {"test":true}
|
||||
const res = parseData('eyJ0ZXN0Ijp0cnVlfQ==');
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
it('returns an object if the payload parses and looks fine', () => {
|
||||
// Encoded: {"messages": [], "prices": {}, "signatures": [], "timestamp": "123"}
|
||||
const res = parseData(
|
||||
'eyJtZXNzYWdlcyI6IFtdLCAicHJpY2VzIjoge30sICJzaWduYXR1cmVzIjogW10sICJ0aW1lc3RhbXAiOiAiMTIzIn0='
|
||||
);
|
||||
expect(typeof res.prices).toEqual('object');
|
||||
expect(Array.isArray(res.messages)).toBeTruthy();
|
||||
expect(Array.isArray(res.signatures)).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,64 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { NestedDataList } from '../../../nested-data-list';
|
||||
import { OpenOraclePrices } from './open-oracle/open-oracle-prices';
|
||||
|
||||
interface OpenOracleDataProps {
|
||||
payload: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes Open Oracle data based on it's main parts:
|
||||
* - messages
|
||||
* - signatures
|
||||
* - prices
|
||||
*/
|
||||
export const OpenOracleData = ({ payload }: OpenOracleDataProps) => {
|
||||
const decodedSubmission = parseData(payload);
|
||||
|
||||
if (!decodedSubmission) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const date = decodedSubmission.timestamp;
|
||||
const formattedDate = new Date(date * 1000).toLocaleString();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<span>{formattedDate}</span>
|
||||
<code>
|
||||
<OpenOraclePrices
|
||||
prices={decodedSubmission.prices}
|
||||
messages={decodedSubmission.messages}
|
||||
signatures={decodedSubmission.signatures}
|
||||
/>
|
||||
</code>
|
||||
<details data-testid="decoded-payload">
|
||||
<summary>{t('Decoded payload')}</summary>
|
||||
<NestedDataList data={decodedSubmission} />
|
||||
</details>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely parses the Open Oracle payload
|
||||
*
|
||||
* @param payload base64 encoded JSON
|
||||
* @returns Object or null
|
||||
*/
|
||||
export function parseData(payload: string) {
|
||||
try {
|
||||
if (!payload || typeof payload !== 'string') {
|
||||
throw new Error('Not a string');
|
||||
}
|
||||
|
||||
const res = JSON.parse(atob(payload));
|
||||
if (!res.prices || !res.messages || !res.signatures) {
|
||||
throw new Error('Not an open oracle format message');
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { OpenOraclePrices } from './open-oracle-prices';
|
||||
import type { Price } from './open-oracle-prices';
|
||||
|
||||
describe('OpenOraclePrices', () => {
|
||||
it('Will not render with no prices', () => {
|
||||
const p = undefined as unknown as Price;
|
||||
const m: string[] = [];
|
||||
const s: string[] = [];
|
||||
const screen = render(
|
||||
<OpenOraclePrices prices={p} messages={m} signatures={s} />
|
||||
);
|
||||
expect(screen.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('Will not render with no messages', () => {
|
||||
const m = undefined as unknown as string[];
|
||||
const p: Price = {};
|
||||
const s: string[] = [];
|
||||
const screen = render(
|
||||
<OpenOraclePrices prices={p} messages={m} signatures={s} />
|
||||
);
|
||||
expect(screen.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('will not render with no signatures', () => {
|
||||
const s = undefined as unknown as string[];
|
||||
const m: string[] = [];
|
||||
const p: Price = {};
|
||||
const screen = render(
|
||||
<OpenOraclePrices prices={p} messages={m} signatures={s} />
|
||||
);
|
||||
expect(screen.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('will not render with mismatched prices/signatures', () => {
|
||||
const p: Price = { ETH: '123' };
|
||||
const m = ['0x123', '0x456'];
|
||||
const s: string[] = [];
|
||||
const screen = render(
|
||||
<OpenOraclePrices prices={p} messages={m} signatures={s} />
|
||||
);
|
||||
expect(screen.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('will not render with mismatched signatures and messages', () => {
|
||||
const p: Price = { ETH: '123', BTC: '12' };
|
||||
const s = ['0xcac', '0xb33f'];
|
||||
const m = ['0x1204o42c', '0xb3ddd3f', '0x9999'];
|
||||
|
||||
const screen = render(
|
||||
<OpenOraclePrices prices={p} messages={m} signatures={s} />
|
||||
);
|
||||
expect(screen.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders a table when there are prices and signatures and messages', () => {
|
||||
const p: Price = { ETH: '123', BTC: '12' };
|
||||
const s = ['0xcac', '0xb33f'];
|
||||
const m = ['0x120442c', '0xb3ddd3f'];
|
||||
|
||||
const screen = render(
|
||||
<OpenOraclePrices prices={p} messages={m} signatures={s} />
|
||||
);
|
||||
expect(screen.getByTestId('openoracleprices')).toBeInTheDocument();
|
||||
|
||||
// Table headings
|
||||
expect(screen.getByText('Asset')).toBeInTheDocument();
|
||||
expect(screen.getByText('Price')).toBeInTheDocument();
|
||||
expect(screen.getByText('Signature')).toBeInTheDocument();
|
||||
expect(screen.getByText('Message')).toBeInTheDocument();
|
||||
// Disabled - see vegaprotocol/frontend-monorepo#/2726
|
||||
// expect(screen.getByText('Signer')).toBeInTheDocument();
|
||||
|
||||
// One row per asset
|
||||
expect(screen.getByText('ETH')).toBeInTheDocument();
|
||||
expect(screen.getByText('123')).toBeInTheDocument();
|
||||
expect(screen.getByText('0xcac')).toBeInTheDocument();
|
||||
expect(screen.getByText('0xb33f')).toBeInTheDocument();
|
||||
// Does not test signer element for this row
|
||||
|
||||
expect(screen.getByText('BTC')).toBeInTheDocument();
|
||||
expect(screen.getByText('12')).toBeInTheDocument();
|
||||
expect(screen.getByText('0x120442c')).toBeInTheDocument();
|
||||
expect(screen.getByText('0xb3ddd3f')).toBeInTheDocument();
|
||||
// Does not test signer element for this row
|
||||
});
|
||||
});
|
@ -0,0 +1,132 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { CopyWithTooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { TableRow } from '../../../../table';
|
||||
import { TruncateInline } from '../../../../truncate/truncate';
|
||||
import { utils } from 'ethers';
|
||||
|
||||
export type Price = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
interface OpenOraclePricesProps {
|
||||
prices: Price;
|
||||
signatures: string[];
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Oracle price table, showing the entries in this
|
||||
* decoded message
|
||||
*
|
||||
* Notes:
|
||||
* - Signer is derived by recovering the address from the
|
||||
* signature and the message. This is currently disabled
|
||||
* as the implementation is incorrect
|
||||
*/
|
||||
export function OpenOraclePrices({
|
||||
prices,
|
||||
messages,
|
||||
signatures,
|
||||
}: OpenOraclePricesProps) {
|
||||
if (
|
||||
!prices ||
|
||||
!messages ||
|
||||
!signatures ||
|
||||
Object.keys(prices).length !== messages.length ||
|
||||
signatures.length !== messages.length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<table data-testid="openoracleprices">
|
||||
<thead>
|
||||
<TableRow modifier="bordered">
|
||||
<th>{t('Asset')}</th>
|
||||
<th className="text-right">{t('Price')}</th>
|
||||
<th>{t('Signature')}</th>
|
||||
<th>{t('Message')}</th>
|
||||
{/* <th>{t('Signer')}</th> */}
|
||||
</TableRow>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(prices).map((k: string, i: number) => {
|
||||
if (k && prices[k]) {
|
||||
const message = messages[i];
|
||||
const signature = signatures[i];
|
||||
return (
|
||||
<OpenOraclePrice
|
||||
key={`price-${i}`}
|
||||
asset={k}
|
||||
value={prices[k]}
|
||||
message={message}
|
||||
signature={signature}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
type OpenOraclePriceProps = {
|
||||
asset: string;
|
||||
value: string;
|
||||
message: string;
|
||||
signature: string;
|
||||
};
|
||||
|
||||
export function OpenOraclePrice({
|
||||
asset,
|
||||
value,
|
||||
signature,
|
||||
message,
|
||||
}: OpenOraclePriceProps) {
|
||||
// const addr = getAddressFromMessageAndSignature(message, signature);
|
||||
return (
|
||||
<TableRow modifier="bordered" key={`${asset}`}>
|
||||
<td className="px-4">{asset}</td>
|
||||
<td className="px-4 text-right">{value}</td>
|
||||
<td className="px-4">
|
||||
<CopyWithTooltip text={signature}>
|
||||
<button>
|
||||
<TruncateInline text={signature} startChars={5} endChars={5} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</td>
|
||||
<td className="px-4">
|
||||
<CopyWithTooltip text={message}>
|
||||
<button>
|
||||
<TruncateInline text={message} startChars={5} endChars={5} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</td>
|
||||
{/*<td className="px-4">
|
||||
<CopyWithTooltip text={message}>
|
||||
<button>
|
||||
<TruncateInline text={addr} startChars={5} endChars={5} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</td> */}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function getAddressFromMessageAndSignature(
|
||||
message: string,
|
||||
signature: string
|
||||
) {
|
||||
try {
|
||||
const m = utils.hashMessage(utils.arrayify(utils.keccak256(message)));
|
||||
const s = utils.splitSignature(
|
||||
signature.slice(0, 130) + signature.slice(-2)
|
||||
);
|
||||
|
||||
return utils.recoverAddress(m, s);
|
||||
} catch (e) {
|
||||
return '-';
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableWithTbody } from '../../table';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import { OpenOracleData } from './oracle-data/data-submission-open-oracle';
|
||||
import { JSONOracleData } from './oracle-data/data-submission-json-oracle';
|
||||
|
||||
export type OracleSubmissionSource =
|
||||
components['schemas']['OracleDataSubmissionOracleSource'];
|
||||
|
||||
interface TxDetailsDataSubmissionProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Someone cancelled an order
|
||||
*/
|
||||
export const TxDetailsDataSubmission = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsDataSubmissionProps) => {
|
||||
if (!txData || !txData.command.oracleDataSubmission) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const type: OracleSubmissionSource =
|
||||
txData.command.oracleDataSubmission.source;
|
||||
if (!type) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const payload = txData.command.oracleDataSubmission.payload;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared
|
||||
txData={txData}
|
||||
pubKey={pubKey}
|
||||
blockData={blockData}
|
||||
/>
|
||||
</TableWithTbody>
|
||||
{type === 'ORACLE_SOURCE_OPEN_ORACLE' ? (
|
||||
<OpenOracleData payload={payload} />
|
||||
) : (
|
||||
<JSONOracleData payload={payload} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -19,6 +19,7 @@ import { TxDetailsUndelegate } from './tx-undelegation';
|
||||
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
||||
import { TxDetailsLiquidityAmendment } from './tx-liquidity-amend';
|
||||
import { TxDetailsLiquidityCancellation } from './tx-liquidity-cancel';
|
||||
import { TxDetailsDataSubmission } from './tx-data-submission';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@ -82,6 +83,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
switch (txData.type) {
|
||||
case 'Submit Order':
|
||||
return TxDetailsOrder;
|
||||
case 'Submit Oracle Data':
|
||||
return TxDetailsDataSubmission;
|
||||
case 'Cancel Order':
|
||||
return TxDetailsOrderCancel;
|
||||
case 'Amend Order':
|
||||
|
Loading…
Reference in New Issue
Block a user