Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
435af22ea7 | ||
|
|
52c38e04bf | ||
|
|
fc52deaf6d | ||
|
|
129274a8e6 | ||
|
|
3fadda15fd | ||
|
|
7ea7edc1e2 | ||
|
|
dad953b45b | ||
|
|
ed1363b035 | ||
|
|
b2ab4f49d9 | ||
|
|
a60379b40f | ||
|
|
7db2a58f9d | ||
|
|
7279556bf7 | ||
|
|
52cc2ef7df | ||
|
|
a218da93a6 | ||
|
|
10fe82dea8 | ||
|
|
b0de4dfbf3 | ||
|
|
cd0e2e41a8 | ||
|
|
dc3330d55e | ||
|
|
a016feab2a | ||
|
|
68446ccf65 | ||
|
|
c7aec5cfcb | ||
|
|
f58144d785 | ||
|
|
415b902f53 | ||
|
|
165b131b27 | ||
|
|
0cff5895c7 | ||
|
|
31881f8e5a | ||
|
|
346af6118d | ||
|
|
bcbc3bb146 | ||
|
|
d7440cfe54 | ||
|
|
801606e37f | ||
|
|
01e7b8caee | ||
|
|
e883ef5c5e | ||
|
|
2f391cde5b | ||
|
|
3620314dca | ||
|
|
9bdd1ed387 | ||
|
|
37dc8bf110 | ||
|
|
3754043f8b | ||
|
|
1ee45b6135 | ||
|
|
a51410c722 | ||
|
|
91173d2434 | ||
|
|
8dd33c285e | ||
|
|
6a7385823a | ||
|
|
9053b343d1 | ||
|
|
6562107578 | ||
|
|
8f5bedc12a | ||
|
|
3044f3bdf7 | ||
|
|
5eb8fb64c7 | ||
|
|
ae84e16b9b | ||
|
|
5e5cc1c587 | ||
|
|
a3aa84b983 | ||
|
|
49374c8e7c |
@@ -40,10 +40,12 @@ jobs:
|
||||
- name: See affected apps
|
||||
run: |
|
||||
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
|
||||
echo -n "Affected projects: $affected"
|
||||
projects=""
|
||||
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
|
||||
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
|
||||
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
|
||||
if [[ -z "$projects" ]]; then projects+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
|
||||
projects=${projects%?}
|
||||
projects=[${projects// /,}]
|
||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||
|
||||
-11
@@ -2,16 +2,5 @@ query ExplorerSettlementAssetForMarket($id: ID!) {
|
||||
market(id: $id) {
|
||||
id
|
||||
decimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-12
@@ -8,7 +8,7 @@ export type ExplorerSettlementAssetForMarketQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerSettlementAssetForMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', product: { __typename?: 'Future', settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
|
||||
export type ExplorerSettlementAssetForMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number } | null };
|
||||
|
||||
|
||||
export const ExplorerSettlementAssetForMarketDocument = gql`
|
||||
@@ -16,17 +16,6 @@ export const ExplorerSettlementAssetForMarketDocument = gql`
|
||||
market(id: $id) {
|
||||
id
|
||||
decimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+2
-2
@@ -91,7 +91,7 @@ describe('LiquidityProvisionDetails component', () => {
|
||||
expect(res.getByTestId('SIDE_SELL-40-1')).toBeInTheDocument();
|
||||
expect(res.getByText('-1')).toBeInTheDocument();
|
||||
expect(res.getByText('Best Bid')).toBeInTheDocument();
|
||||
expect(res.getByText('40% (normalised from: 20%)')).toBeInTheDocument();
|
||||
expect(res.getByText('40%')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles a missing offset gracefully (should not happen)', () => {
|
||||
@@ -125,7 +125,7 @@ describe('LiquidityProvisionDetails component', () => {
|
||||
);
|
||||
// Row test ids and keys are based on the side, reference and proportion - and that proportion is scaled
|
||||
expect(res.getByTestId('SIDE_SELL-40-1')).toBeInTheDocument();
|
||||
expect(res.getByText('40% (normalised from: 20%)')).toBeInTheDocument();
|
||||
expect(res.getByText('40%')).toBeInTheDocument();
|
||||
expect(res.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-5
@@ -67,11 +67,7 @@ export function LiquidityProvisionDetailsRow({
|
||||
<td className="text-center">
|
||||
{order.reference ? LiquidityReferenceLabel[order.reference] : '-'}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{proportion === order.proportion
|
||||
? `${proportion}%`
|
||||
: `${proportion}% (normalised from: ${order.proportion}%)`}{' '}
|
||||
</td>
|
||||
<td className="text-center">{proportion}%</td>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
-9
@@ -13,15 +13,6 @@ const decimalsMock: ExplorerSettlementAssetForMarketQuery = {
|
||||
id: '123',
|
||||
__typename: 'Market',
|
||||
decimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
decimals: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+2
-4
@@ -36,7 +36,7 @@ export function LiquidityProvisionOffset({
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the work of formatting the number now we have the settlement decimal places.
|
||||
* Does the work of formatting the number now we have the market decimal places.
|
||||
* If no market data is assigned (i.e. during loading, or if the market doesn't exist)
|
||||
* this function will return the unformatted number
|
||||
*
|
||||
@@ -49,9 +49,7 @@ export function getFormattedOffset(
|
||||
offset: string,
|
||||
data?: ExplorerSettlementAssetForMarketQuery
|
||||
) {
|
||||
const decimals =
|
||||
data?.market?.tradableInstrument.instrument.product.settlementAsset
|
||||
.decimals;
|
||||
const decimals = data?.market?.decimalPlaces;
|
||||
|
||||
if (!decimals) {
|
||||
return offset;
|
||||
|
||||
+2
-2
@@ -203,7 +203,7 @@ describe('LiquidityProvisionDetails component', () => {
|
||||
};
|
||||
|
||||
const res = renderComponent(mock);
|
||||
expect(res.getByText('45% (normalised from: 25%)')).toBeInTheDocument();
|
||||
expect(res.getByText('55% (normalised from: 30%)')).toBeInTheDocument();
|
||||
expect(res.getByText('45%')).toBeInTheDocument();
|
||||
expect(res.getByText('55%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+45
@@ -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}');
|
||||
});
|
||||
});
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
+58
@@ -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();
|
||||
});
|
||||
});
|
||||
+64
@@ -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;
|
||||
}
|
||||
}
|
||||
+88
@@ -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
|
||||
});
|
||||
});
|
||||
+132
@@ -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':
|
||||
|
||||
@@ -27,7 +27,7 @@ export const TxDetailsLiquidityCancellation = ({
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsLiquidityCancellationProps) => {
|
||||
if (!txData || !txData.command.liquidityProvisionAmendment) {
|
||||
if (!txData || !txData.command.liquidityProvisionCancellation) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "74614.7487514014739871627",
|
||||
"locked_amount": "74377.3343235149335839749",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -71,7 +71,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1747.860227157102",
|
||||
"locked_amount": "1734.125553266178",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -450,7 +450,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "74546.672181553510075935",
|
||||
"locked_amount": "74309.47436445978496023",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -516,7 +516,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "37944.8998795027944",
|
||||
"locked_amount": "37773.4128805175029",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -709,7 +709,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "3222.523623795028",
|
||||
"locked_amount": "3208.826579147641",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -920,7 +920,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "97499.58",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "16345.542696388554157101",
|
||||
"locked_amount": "16112.873733795746655318",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "97499.58",
|
||||
@@ -953,7 +953,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "135173.4239508",
|
||||
"total_removed": "98230.390980249184455396",
|
||||
"locked_amount": "22341.53492574140235843834708",
|
||||
"locked_amount": "22023.516622497674397517810344",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "135173.4239508",
|
||||
@@ -999,7 +999,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "32499.86",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "6876.287539946593035582",
|
||||
"locked_amount": "6778.4077253618634870844",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "32499.86",
|
||||
@@ -1032,7 +1032,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "10833.29",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "2238.1648683377590511606",
|
||||
"locked_amount": "2206.3059384936688557712",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10833.29",
|
||||
@@ -1065,7 +1065,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "22749.93",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "8366.760162941752814571",
|
||||
"locked_amount": "8247.664367621075063898",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "6500",
|
||||
@@ -1203,8 +1203,8 @@
|
||||
"tranche_start": "2022-11-01T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-01T00:00:00.000Z",
|
||||
"total_added": "22500",
|
||||
"total_removed": "3338.59298265",
|
||||
"locked_amount": "11964.06106123388625",
|
||||
"total_removed": "3539.640404325",
|
||||
"locked_amount": "11839.76605662983325",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -1228,6 +1228,11 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x63c8fb032e621103aa45aa0cba24fdedc19f9640271b6d59058b576a00862340"
|
||||
},
|
||||
{
|
||||
"amount": "201.047421675",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xbca53bb0a17ad84f130058da0d3773b8a649ec4270702b3541d75fcbeba5899a"
|
||||
},
|
||||
{
|
||||
"amount": "286.3317219",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -1288,6 +1293,12 @@
|
||||
"tranche_id": 33,
|
||||
"tx": "0x63c8fb032e621103aa45aa0cba24fdedc19f9640271b6d59058b576a00862340"
|
||||
},
|
||||
{
|
||||
"amount": "201.047421675",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 33,
|
||||
"tx": "0xbca53bb0a17ad84f130058da0d3773b8a649ec4270702b3541d75fcbeba5899a"
|
||||
},
|
||||
{
|
||||
"amount": "286.3317219",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -1332,8 +1343,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "7500",
|
||||
"withdrawn_tokens": "3338.59298265",
|
||||
"remaining_tokens": "4161.40701735"
|
||||
"withdrawn_tokens": "3539.640404325",
|
||||
"remaining_tokens": "3960.359595675"
|
||||
},
|
||||
{
|
||||
"address": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
|
||||
@@ -1358,7 +1369,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "928642.9598472029154",
|
||||
"locked_amount": "681601.673289015032543106",
|
||||
"locked_amount": "676287.4161622965221101968",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -1710,7 +1721,7 @@
|
||||
"tranche_end": "2023-02-01T00:00:00.000Z",
|
||||
"total_added": "42500",
|
||||
"total_removed": "24434.0787288",
|
||||
"locked_amount": "1673.2583849134443",
|
||||
"locked_amount": "1442.306857638887255",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "12500",
|
||||
@@ -33086,7 +33097,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "441714.6865497406814",
|
||||
"locked_amount": "1071885.69354993468475576716",
|
||||
"locked_amount": "1063719.625100700820753200245",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -34366,8 +34377,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "544810.37930337016827952",
|
||||
"locked_amount": "9100525.4333294991121317413486608148229251",
|
||||
"total_removed": "545472.50137003958740452",
|
||||
"locked_amount": "9071569.0789202252235565659217193396309814",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -34966,6 +34977,11 @@
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0x7deb028bfbd706c2281d6344cca117acc153a35ad68dd9a58f3ad7867f643d37"
|
||||
},
|
||||
{
|
||||
"amount": "662.122066669419125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x6d913b124af23621f8b97087a51d4cd912c6a8d9cca325cb756b56d4970f3f62"
|
||||
},
|
||||
{
|
||||
"amount": "434.254023685890375",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -36494,6 +36510,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x2221aafeab49a64ec0b85aa53438b6c66a06eff11fee7ab0a254f33657e47aab"
|
||||
},
|
||||
{
|
||||
"amount": "662.122066669419125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x6d913b124af23621f8b97087a51d4cd912c6a8d9cca325cb756b56d4970f3f62"
|
||||
},
|
||||
{
|
||||
"amount": "434.254023685890375",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -37510,8 +37532,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "259998.8875",
|
||||
"withdrawn_tokens": "110560.080031402588125",
|
||||
"remaining_tokens": "149438.807468597411875"
|
||||
"withdrawn_tokens": "111222.20209807200725",
|
||||
"remaining_tokens": "148776.68540192799275"
|
||||
},
|
||||
{
|
||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||
@@ -39646,8 +39668,8 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "3705868.24629153380213143",
|
||||
"locked_amount": "2680101.746355963103585199029496367",
|
||||
"total_removed": "3706973.15022981060693393",
|
||||
"locked_amount": "2653369.424853388202023095424913904",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129284.449",
|
||||
@@ -39936,6 +39958,16 @@
|
||||
"user": "0x66827bCD635f2bB1779d68c46aEB16541bCA6ba8",
|
||||
"tx": "0x77247f5a8850ad76c194b1e7481ddf61b1106a3f9df35117b7e146bd70c66616"
|
||||
},
|
||||
{
|
||||
"amount": "645.31140451891061",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xe96d96d780f7aec7b2e42a190a91993ac93bb61d74b12ff72e87c0ee4a2e2712"
|
||||
},
|
||||
{
|
||||
"amount": "459.5925337578941925",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xc1e8ef457dbeb52bd2d02aefd418a9a67bd17f016c1ca6a1e80716d86ce3c132"
|
||||
},
|
||||
{
|
||||
"amount": "12758.73908063512839093",
|
||||
"user": "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E",
|
||||
@@ -42702,6 +42734,18 @@
|
||||
"tranche_id": 3,
|
||||
"tx": "0x074c8b4e60fda9e8dba220410f5a5805076e763589315992ca70db527a0bc674"
|
||||
},
|
||||
{
|
||||
"amount": "645.31140451891061",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xe96d96d780f7aec7b2e42a190a91993ac93bb61d74b12ff72e87c0ee4a2e2712"
|
||||
},
|
||||
{
|
||||
"amount": "459.5925337578941925",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xc1e8ef457dbeb52bd2d02aefd418a9a67bd17f016c1ca6a1e80716d86ce3c132"
|
||||
},
|
||||
{
|
||||
"amount": "762.7902369187144135",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -45014,8 +45058,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "359123.469575",
|
||||
"withdrawn_tokens": "292710.1938103126453605",
|
||||
"remaining_tokens": "66413.2757646873546395"
|
||||
"withdrawn_tokens": "293815.097748589450163",
|
||||
"remaining_tokens": "65308.371826410549837"
|
||||
},
|
||||
{
|
||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||
@@ -46326,7 +46370,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "2622261.560853924298939789",
|
||||
"locked_amount": "742021.005565470993478249377386312",
|
||||
"locked_amount": "731458.903664069114159753480553424",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "552496.6455",
|
||||
@@ -48221,7 +48265,7 @@
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "29683.1054262326685",
|
||||
"locked_amount": "169846.415302828370245964120040588",
|
||||
"locked_amount": "168552.45507780998384909840791476",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import type { UserTrancheBalance } from '../../contexts/app-state/app-state-context';
|
||||
|
||||
export interface AssociationBreakdown {
|
||||
|
||||
@@ -20,6 +20,24 @@ query Proposal($proposalId: ID!) {
|
||||
... on NewMarket {
|
||||
decimalPlaces
|
||||
metadata
|
||||
lpPriceRange
|
||||
riskParameters {
|
||||
... on LogNormalRiskModel {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
mu
|
||||
r
|
||||
sigma
|
||||
}
|
||||
}
|
||||
... on SimpleRiskModel {
|
||||
params {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
}
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
@@ -32,6 +50,7 @@ query Proposal($proposalId: ID!) {
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
import type ethers from 'ethers';
|
||||
import type { GetState, SetState } from 'zustand';
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface TxData {
|
||||
tx: ethers.ContractTransaction;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
const assetSelectField = 'select[name="asset"]';
|
||||
const toAddressField = 'input[name="to"]';
|
||||
const amountField = 'input[name="amount"]';
|
||||
const formFieldError = 'input-error-text';
|
||||
|
||||
const ASSET_EURO = 1;
|
||||
|
||||
describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.mockWeb3Provider();
|
||||
@@ -43,7 +46,8 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
|
||||
it('invalid amount', () => {
|
||||
// Deposit amount smaller than minimum viable for selected asset
|
||||
// Select an amount so that we have a known decimal places value to work with
|
||||
cy.get(assetSelectField).select('Euro');
|
||||
selectAsset(ASSET_EURO);
|
||||
|
||||
cy.get(amountField)
|
||||
.clear()
|
||||
.type('0.00000000000000000000000000000000001')
|
||||
|
||||
@@ -161,6 +161,14 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('liquidity price range displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Liquidity price range').click();
|
||||
|
||||
validateMarketDataRow(0, 'Liquidity Price Range', '2.00%');
|
||||
validateMarketDataRow(1, 'LP Volume Min', '0.05634 tBTC');
|
||||
validateMarketDataRow(2, 'LP Volume Max', '0.05864 tBTC');
|
||||
});
|
||||
|
||||
it('oracle displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Oracle').click();
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
const formFieldError = 'input-error-text';
|
||||
const toAddressField = 'input[name="to"]';
|
||||
const assetSelectField = 'select[name="asset"]';
|
||||
const amountField = 'input[name="amount"]';
|
||||
const useMaximumAmount = 'use-maximum';
|
||||
const submitWithdrawBtn = 'submit-withdrawal';
|
||||
const ethAddressValue = Cypress.env('ETHEREUM_WALLET_ADDRESS');
|
||||
const asset1Name = 'Sepolia tBTC';
|
||||
const asset2Name = 'Euro';
|
||||
|
||||
const ASSET_SEPOLIA_TBTC = 2;
|
||||
const ASSET_EURO = 1;
|
||||
|
||||
describe('withdraw form validation', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
@@ -40,7 +41,7 @@ describe('withdraw form validation', { tags: '@smoke' }, () => {
|
||||
cy.get(toAddressField).should('have.value', ethAddressValue);
|
||||
});
|
||||
it('min amount', () => {
|
||||
selectAsset(asset1Name);
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.get(amountField).clear().type('0');
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
cy.get('[data-testid="input-error-text"]').should(
|
||||
@@ -49,7 +50,7 @@ describe('withdraw form validation', { tags: '@smoke' }, () => {
|
||||
);
|
||||
});
|
||||
it('max amount', () => {
|
||||
selectAsset(asset2Name); // Will be above maximum because the vega wallet doesn't have any collateral
|
||||
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
|
||||
cy.get(amountField).clear().type('1001', { delay: 100 });
|
||||
cy.getByTestId(submitWithdrawBtn).click();
|
||||
cy.get('[data-testid="input-error-text"]').should(
|
||||
@@ -59,7 +60,7 @@ describe('withdraw form validation', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('can set amount using use maximum button', () => {
|
||||
selectAsset(asset1Name);
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId(useMaximumAmount).click();
|
||||
cy.get(amountField).should('have.value', '1000.00000');
|
||||
});
|
||||
@@ -87,7 +88,7 @@ describe('withdraw actions', { tags: '@regression' }, () => {
|
||||
});
|
||||
|
||||
it('triggers transaction when submitted', () => {
|
||||
selectAsset(asset1Name);
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.getByTestId('BALANCE_AVAILABLE_label').should(
|
||||
'contain.text',
|
||||
'Balance available'
|
||||
@@ -111,12 +112,3 @@ describe('withdraw actions', { tags: '@regression' }, () => {
|
||||
it.skip('creates a withdrawal on submit'); // Needs capsule
|
||||
it.skip('creates a withdrawal on submit and prompts to complete withdrawal'); // Needs capsule
|
||||
});
|
||||
|
||||
const selectAsset = (assetName: string) => {
|
||||
cy.get(assetSelectField).select(assetName);
|
||||
// The asset only gets set once the queries (getWithdrawThreshold, getDelay)
|
||||
// against the Ethereum change resolve, we should fix this but for now just force
|
||||
// some wait time
|
||||
// eslint-disable-next-line
|
||||
cy.wait(100);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const selectAsset = (assetIndex: number) => {
|
||||
cy.log(`selecting asset: ${assetIndex}`);
|
||||
cy.getByTestId('select-asset').click();
|
||||
cy.get('[data-testid="rich-select-option"]').eq(assetIndex).click();
|
||||
|
||||
// The asset only gets set once the queries (getWithdrawThreshold, getDelay)
|
||||
// against the Ethereum change resolve, we should fix this but for now just force
|
||||
// some wait time
|
||||
// eslint-disable-next-line
|
||||
cy.wait(100);
|
||||
};
|
||||
@@ -8,6 +8,3 @@ NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"
|
||||
NX_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
|
||||
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
|
||||
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
|
||||
@@ -139,16 +139,19 @@ const AccountHistoryManager = ({
|
||||
<div className="h-full w-full flex flex-col gap-8">
|
||||
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
@@ -164,10 +167,13 @@ const AccountHistoryManager = ({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{assets.map((a) => (
|
||||
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>
|
||||
|
||||
@@ -43,8 +43,8 @@ export const HeaderStat = ({
|
||||
testId?: string;
|
||||
}) => {
|
||||
const itemClass =
|
||||
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default';
|
||||
const itemHeading = 'text-neutral-500 dark:text-neutral-400';
|
||||
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default text-neutral-500 dark:text-neutral-400';
|
||||
const itemHeading = 'text-black dark:text-white';
|
||||
|
||||
return (
|
||||
<div data-testid={testId} className={itemClass}>
|
||||
|
||||
+3
-2
@@ -3,6 +3,7 @@ import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletConnectButton } from './vega-wallet-connect-button';
|
||||
import { truncateByChars } from '@vegaprotocol/react-helpers';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const mockUpdateDialogOpen = jest.fn();
|
||||
jest.mock('@vegaprotocol/wallet', () => ({
|
||||
@@ -31,7 +32,7 @@ it('Not connected', () => {
|
||||
expect(mockUpdateDialogOpen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Connected', () => {
|
||||
it('Connected', async () => {
|
||||
const pubKey = { publicKey: '123456__123456', name: 'test' };
|
||||
render(
|
||||
generateJsx({
|
||||
@@ -42,6 +43,6 @@ it('Connected', () => {
|
||||
|
||||
const button = screen.getByTestId('manage-vega-wallet');
|
||||
expect(button).toHaveTextContent(truncateByChars(pubKey.publicKey));
|
||||
fireEvent.click(button);
|
||||
userEvent.click(button);
|
||||
expect(mockUpdateDialogOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -142,15 +142,21 @@ export const VegaWalletConnectButton = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="hidden lg:block">
|
||||
<DropdownMenu open={dropdownOpen}>
|
||||
<DropdownMenuTrigger
|
||||
data-testid="manage-vega-wallet"
|
||||
onClick={() => setDropdownOpen((curr) => !curr)}
|
||||
>
|
||||
{activeKey && <span className="uppercase">{activeKey.name}</span>}
|
||||
{': '}
|
||||
{truncateByChars(pubKey)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
open={dropdownOpen}
|
||||
trigger={
|
||||
<DropdownMenuTrigger
|
||||
data-testid="manage-vega-wallet"
|
||||
onClick={() => setDropdownOpen((curr) => !curr)}
|
||||
>
|
||||
{activeKey && (
|
||||
<span className="uppercase">{activeKey.name}</span>
|
||||
)}
|
||||
{': '}
|
||||
{truncateByChars(pubKey)}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent
|
||||
onInteractOutside={() => setDropdownOpen(false)}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { ETHERSCAN_TX, useEtherscanLink } from '@vegaprotocol/environment';
|
||||
import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
@@ -18,60 +19,59 @@ const intentMap: { [s in EthTxStatus]: Intent } = {
|
||||
Requested: Intent.Warning,
|
||||
Pending: Intent.Warning,
|
||||
Error: Intent.Danger,
|
||||
Complete: Intent.Success,
|
||||
Complete: Intent.Warning,
|
||||
Confirmed: Intent.Success,
|
||||
};
|
||||
|
||||
const isWithdrawTransaction = (tx: EthStoredTxState) =>
|
||||
tx.methodName === 'withdraw_asset';
|
||||
|
||||
const isDepositTransaction = (tx: EthStoredTxState) =>
|
||||
tx.methodName === 'withdraw_asset';
|
||||
|
||||
const EthTransactionDetails = ({ tx }: { tx: EthStoredTxState }) => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
if (!data) return null;
|
||||
const { data: assets } = useAssetsDataProvider();
|
||||
if (!assets) return null;
|
||||
|
||||
const ETH_WITHDRAW =
|
||||
tx.methodName === 'withdraw_asset' && tx.args.length > 2 && tx.assetId;
|
||||
const ETH_DEPOSIT =
|
||||
tx.methodName === 'deposit_asset' && tx.args.length > 2 && tx.assetId;
|
||||
|
||||
let label = '';
|
||||
if (ETH_WITHDRAW) label = t('Withdraw');
|
||||
if (ETH_DEPOSIT) label = t('Deposit');
|
||||
|
||||
if (ETH_WITHDRAW || ETH_DEPOSIT) {
|
||||
const asset = data.find((a) => a.id === tx.assetId);
|
||||
const isWithdraw = isWithdrawTransaction(tx);
|
||||
const isDeposit = isDepositTransaction(tx);
|
||||
|
||||
let assetInfo: ReactNode;
|
||||
if ((isWithdraw || isDeposit) && tx.args.length > 2 && tx.assetId) {
|
||||
const asset = assets.find((a) => a.id === tx.assetId);
|
||||
if (asset) {
|
||||
const num = formatNumber(
|
||||
toBigNum(tx.args[1], asset.decimals),
|
||||
asset.decimals
|
||||
);
|
||||
const details = (
|
||||
let label = '';
|
||||
if (isWithdraw) label = t('Withdraw');
|
||||
if (isDeposit) label = t('Deposit');
|
||||
assetInfo = (
|
||||
<div className="mt-[5px]">
|
||||
<span className="font-mono text-xs p-1 bg-gray-100 rounded">
|
||||
{label} {num} {asset.symbol}
|
||||
{label}{' '}
|
||||
{formatNumber(toBigNum(tx.args[1], asset.decimals), asset.decimals)}{' '}
|
||||
{asset.symbol}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{details}
|
||||
{tx.requiresConfirmation &&
|
||||
[EthTxStatus.Pending].includes(tx.status) && (
|
||||
<div className="mt-[10px]">
|
||||
<span className="font-mono text-xs">
|
||||
{t('Awaiting confirmations')}{' '}
|
||||
{`(${tx.confirmations}/${tx.requiredConfirmations})`}
|
||||
</span>
|
||||
<ProgressBar
|
||||
value={(tx.confirmations / tx.requiredConfirmations) * 100}
|
||||
intent={Intent.Warning}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{assetInfo}
|
||||
{tx.status === EthTxStatus.Pending && (
|
||||
<div className="mt-[10px]">
|
||||
<span className="font-mono text-xs">
|
||||
{t('Awaiting confirmations')}{' '}
|
||||
{`(${tx.confirmations}/${tx.requiredConfirmations})`}
|
||||
</span>
|
||||
<ProgressBar
|
||||
value={(tx.confirmations / tx.requiredConfirmations) * 100}
|
||||
intent={Intent.Warning}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type EthTxToastContentProps = {
|
||||
@@ -93,21 +93,11 @@ const EthTxRequestedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
};
|
||||
|
||||
const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
|
||||
<p>{t('Please wait for your transaction to be confirmed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(ETHERSCAN_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
@@ -130,22 +120,43 @@ const EthTxErrorToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
const EtherscanLink = ({ tx }: EthTxToastContentProps) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
return tx.txHash ? (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(ETHERSCAN_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Transaction completed')}</h3>
|
||||
<p>{t('Your transaction has been completed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(ETHERSCAN_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<h3 className="font-bold">{t('Transaction confirmed')}</h3>
|
||||
<p>{t('Your transaction has been confirmed')}</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EthTxCompletedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
const isDeposit = isDepositTransaction(tx);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">
|
||||
{t('Processing')} {isDeposit && 'deposit'}
|
||||
</h3>
|
||||
<p>
|
||||
{t('Your transaction has been completed.')}
|
||||
{isDeposit && t('Waiting for deposit confirmation')}
|
||||
</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
@@ -167,10 +178,10 @@ export const useEthereumTransactionToasts = () => {
|
||||
if (tx.status === EthTxStatus.Pending) {
|
||||
content = <EthTxPendingToastContent tx={tx} />;
|
||||
}
|
||||
if (
|
||||
tx.status === EthTxStatus.Confirmed ||
|
||||
tx.status === EthTxStatus.Complete
|
||||
) {
|
||||
if (tx.status === EthTxStatus.Complete) {
|
||||
content = <EthTxCompletedToastContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === EthTxStatus.Confirmed) {
|
||||
content = <EthTxConfirmedToastContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
@@ -181,7 +192,7 @@ export const useEthereumTransactionToasts = () => {
|
||||
id: `eth-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
onClose: () => dismissEthTransaction(tx.id),
|
||||
loader: tx.status === EthTxStatus.Pending,
|
||||
loader: [EthTxStatus.Pending, EthTxStatus.Complete].includes(tx.status),
|
||||
content,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ const Details = ({
|
||||
title?: string;
|
||||
}) => (
|
||||
<div className="pt-[5px]" data-testid="vega-tx-details" title={title}>
|
||||
<div className="font-mono text-xs p-2 bg-neutral-100 rounded">
|
||||
<div className="font-mono text-xs p-2 bg-neutral-100 rounded dark:bg-neutral-700 dark:text-white">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import produce from 'immer';
|
||||
|
||||
interface GlobalStore {
|
||||
|
||||
@@ -9,26 +9,23 @@ export const useMarketAccountBalance = (marketId: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [accountBalance, setAccountBalance] = useState<string>('');
|
||||
const [accountDecimals, setAccountDecimals] = useState<number | null>(null);
|
||||
const variables = useMemo(() => {
|
||||
return { partyId: pubKey || '' };
|
||||
}, [pubKey]);
|
||||
const update = useCallback(
|
||||
({ data }: { data: Account[] | null }) => {
|
||||
const account = getMarketAccount({ accounts: data, marketId });
|
||||
if (accountBalance !== account?.balance) {
|
||||
if (account?.balance) {
|
||||
setAccountBalance(account?.balance || '');
|
||||
}
|
||||
if (accountDecimals !== account?.asset.decimals) {
|
||||
if (account?.asset.decimals) {
|
||||
setAccountDecimals(account?.asset.decimals || null);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[accountBalance, accountDecimals, marketId]
|
||||
[marketId]
|
||||
);
|
||||
|
||||
useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey || !marketId,
|
||||
update,
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { onError } from '@apollo/client/link/error';
|
||||
import { RetryLink } from '@apollo/client/link/retry';
|
||||
import ApolloLinkTimeout from 'apollo-link-timeout';
|
||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||
import { localLoggerFactory } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
@@ -68,12 +69,12 @@ export function createClient(base?: string, cacheConfig?: InMemoryCacheConfig) {
|
||||
if (graphQLErrors) {
|
||||
graphQLErrors.forEach((e) => {
|
||||
if (e.extensions && e.extensions['type'] !== NOT_FOUND) {
|
||||
console.log(e);
|
||||
localLoggerFactory({ application: 'apollo-client' }).error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (networkError) {
|
||||
console.log(networkError);
|
||||
localLoggerFactory({ application: 'apollo-client' }).error(networkError);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useAssetsDataProvider } from './assets-data-provider';
|
||||
import { Button, Dialog, Icon, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import { AssetDetailsTable } from './asset-details-table';
|
||||
import { AssetProposalNotification } from '@vegaprotocol/governance';
|
||||
|
||||
|
||||
@@ -60,10 +60,13 @@ export const CandlesChartContainer = ({
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-4 py-2 flex flex-row flex-wrap gap-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
{t(`Interval: ${intervalLabels[interval]}`)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
{t(`Interval: ${intervalLabels[interval]}`)}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={interval}
|
||||
@@ -84,10 +87,13 @@ export const CandlesChartContainer = ({
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Icon name={chartTypeIcon.get(chartType) as IconName} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
<Icon name={chartTypeIcon.get(chartType) as IconName} />
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={chartType}
|
||||
@@ -104,8 +110,9 @@ export const CandlesChartContainer = ({
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>{t('Overlays')}</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={<DropdownMenuTrigger>{t('Overlays')}</DropdownMenuTrigger>}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{Object.values(Overlay).map((overlay) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@@ -128,8 +135,9 @@ export const CandlesChartContainer = ({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>{t('Studies')}</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={<DropdownMenuTrigger>{t('Studies')}</DropdownMenuTrigger>}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{Object.values(Study).map((study) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import {
|
||||
StakingBridge,
|
||||
Token,
|
||||
TokenVesting,
|
||||
TokenFaucetable,
|
||||
CollateralBridge,
|
||||
} from '@vegaprotocol/smart-contracts';
|
||||
import { ethers, Wallet } from 'ethers';
|
||||
|
||||
const vegaWalletContainer = '[data-testid="vega-wallet"]';
|
||||
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
|
||||
const vegaWalletMnemonic = Cypress.env('vegaWalletMnemonic');
|
||||
const vegaWalletPubKey = Cypress.env('vegaWalletPublicKey');
|
||||
const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
|
||||
const vegaTokenAddress = Cypress.env('vegaTokenAddress');
|
||||
const ethWalletPubKey = Cypress.env('ethWalletPublicKey');
|
||||
const ethStakingBridgeContractAddress = Cypress.env(
|
||||
'ethStakingBridgeContractAddress'
|
||||
);
|
||||
const ethProviderUrl = Cypress.env('ethProviderUrl');
|
||||
const getAccount = (number = 0) => `m/44'/60'/0'/0/${number}`;
|
||||
const transactionTimeout = '90000';
|
||||
const Erc20BridgeAddress = '0x9708FF7510D4A7B9541e1699d15b53Ecb1AFDc54';
|
||||
|
||||
before('Vega wallet teardown prep', function () {
|
||||
cy.wrap(new ethers.providers.JsonRpcProvider({ url: ethProviderUrl }), {
|
||||
log: false,
|
||||
}).as('provider');
|
||||
|
||||
cy.wrap(Wallet.fromMnemonic(vegaWalletMnemonic, getAccount(0)).privateKey, {
|
||||
log: false,
|
||||
}).then((privateKey) => {
|
||||
cy.wrap(new Wallet(privateKey, this.provider), { log: false }).as('signer');
|
||||
});
|
||||
|
||||
cy.get('@signer', { log: false }).then((signer) => {
|
||||
cy.wrap(new StakingBridge(ethStakingBridgeContractAddress, signer), {
|
||||
log: false,
|
||||
}).as('stakingBridgeContract');
|
||||
cy.wrap(new TokenVesting(vegaTokenContractAddress, signer), {
|
||||
log: false,
|
||||
}).as('vestingContract');
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('deposit_asset', function (assetEthAddress) {
|
||||
cy.get('@signer', { log: false }).then((signer) => {
|
||||
// Approve asset
|
||||
cy.wrap(
|
||||
new TokenFaucetable(assetEthAddress, signer).approve(
|
||||
Erc20BridgeAddress,
|
||||
'10000000000'
|
||||
)
|
||||
)
|
||||
.then((tx) => {
|
||||
cy.wait_for_transaction(tx);
|
||||
})
|
||||
.then(() => {
|
||||
cy.wrap(new CollateralBridge(Erc20BridgeAddress, signer), {
|
||||
log: false,
|
||||
}).then((bridge) => {
|
||||
// Deposit asset into vega wallet
|
||||
cy.wrap(
|
||||
bridge.deposit_asset(
|
||||
assetEthAddress,
|
||||
'1000000000',
|
||||
'0x' + vegaWalletPubKey
|
||||
),
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
).then((tx) => {
|
||||
cy.wait_for_transaction(tx);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('faucet_asset', function (assetEthAddress) {
|
||||
cy.get('@signer', { log: false }).then((signer) => {
|
||||
cy.wrap(new TokenFaucetable(assetEthAddress, signer), { log: false }).then(
|
||||
(token) => {
|
||||
cy.wrap(token.faucet(), {
|
||||
timeout: transactionTimeout,
|
||||
log: false,
|
||||
}).then((tx) => {
|
||||
cy.wait_for_transaction(tx);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('vega_wallet_teardown', function () {
|
||||
cy.get(vegaWalletContainer).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance)
|
||||
.first()
|
||||
.invoke('text')
|
||||
.then((balance) => {
|
||||
if (balance != '0.00') {
|
||||
cy.vega_wallet_teardown_vesting(this.vestingContract);
|
||||
cy.vega_wallet_teardown_staking(this.stakingBridgeContract);
|
||||
}
|
||||
});
|
||||
cy.get('[data-testid="associated-amount"]', { timeout: 30000 }).should(
|
||||
'contain.text',
|
||||
'0.00'
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(vegaWalletContainer).within(() => {
|
||||
cy.get(vegaWalletAssociatedBalance, { timeout: transactionTimeout }).should(
|
||||
'contain',
|
||||
'0.00',
|
||||
{ timeout: transactionTimeout }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
'vega_wallet_set_specified_approval_amount',
|
||||
function (resetAmount) {
|
||||
cy.highlight(`Setting token approval amount to ${resetAmount}`);
|
||||
cy.wrap(new Token(vegaTokenAddress, this.signer), { log: false }).then(
|
||||
(token) => {
|
||||
cy.wrap(
|
||||
token.approve(
|
||||
ethStakingBridgeContractAddress,
|
||||
resetAmount.concat('000000000000000000')
|
||||
),
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
).then((tx) => {
|
||||
cy.wait_for_transaction(tx);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
'vega_wallet_teardown_staking',
|
||||
(stakingBridgeContract) => {
|
||||
cy.highlight('Tearing down staking tokens from vega wallet if present');
|
||||
cy.wrap(
|
||||
stakingBridgeContract.stake_balance(ethWalletPubKey, vegaWalletPubKey),
|
||||
{
|
||||
timeout: transactionTimeout,
|
||||
log: false,
|
||||
}
|
||||
).then((stake_amount) => {
|
||||
if (String(stake_amount) != '0') {
|
||||
cy.wrap(
|
||||
stakingBridgeContract.remove_stake(stake_amount, vegaWalletPubKey),
|
||||
{ timeout: transactionTimeout, log: false }
|
||||
).then((tx) => {
|
||||
cy.wait_for_transaction(tx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('vega_wallet_teardown_vesting', (vestingContract) => {
|
||||
cy.highlight('Tearing down vesting tokens from vega wallet if present');
|
||||
cy.wrap(vestingContract.stake_balance(ethWalletPubKey, vegaWalletPubKey), {
|
||||
timeout: transactionTimeout,
|
||||
log: false,
|
||||
}).then((vesting_amount) => {
|
||||
if (String(vesting_amount) != '0') {
|
||||
cy.wrap(vestingContract.remove_stake(vesting_amount, vegaWalletPubKey), {
|
||||
timeout: transactionTimeout,
|
||||
log: false,
|
||||
}).then((tx) => {
|
||||
cy.wait_for_transaction(tx);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('wait_for_transaction', (tx) => {
|
||||
cy.wrap(tx.wait(1).catch(cy.log), { timeout: transactionTimeout });
|
||||
});
|
||||
@@ -15,7 +15,6 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
|
||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||
import { usePersistedOrderStore } from '../../hooks/use-persisted-order';
|
||||
import {
|
||||
getDefaultOrder,
|
||||
validateMarketState,
|
||||
@@ -27,6 +26,10 @@ import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
||||
import { SummaryValidationType } from '../../constants';
|
||||
import { useHasNoBalance } from '../../hooks/use-has-no-balance';
|
||||
import type { MarketDealTicket } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
usePersistedOrderStore,
|
||||
usePersistedOrderStoreSubscription,
|
||||
} from '@vegaprotocol/orders';
|
||||
|
||||
export type TransactionStatus = 'default' | 'pending';
|
||||
|
||||
@@ -43,13 +46,13 @@ export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
|
||||
|
||||
export const DealTicket = ({ market, submit }: DealTicketProps) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
// const [persistedOrder, setPersistedOrder] = usePersistedOrder(market);
|
||||
const { getPersistedOrder, setPersistedOrder } = usePersistedOrderStore(
|
||||
(store) => ({
|
||||
getPersistedOrder: store.getOrder,
|
||||
setPersistedOrder: store.setOrder,
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
@@ -58,11 +61,23 @@ export const DealTicket = ({ market, submit }: DealTicketProps) => {
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
} = useForm<DealTicketFormFields>({
|
||||
defaultValues: getPersistedOrder(market.id) || getDefaultOrder(market),
|
||||
});
|
||||
|
||||
const order = watch();
|
||||
|
||||
watch((orderData) => {
|
||||
setPersistedOrder(orderData as DealTicketFormFields);
|
||||
});
|
||||
|
||||
usePersistedOrderStoreSubscription(market.id, (storedOrder) => {
|
||||
if (order.price !== storedOrder.price) {
|
||||
setValue('price', storedOrder.price);
|
||||
}
|
||||
});
|
||||
|
||||
const marketStateError = validateMarketState(market.data.marketState);
|
||||
const hasNoBalance = useHasNoBalance(
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id
|
||||
@@ -90,9 +105,6 @@ export const DealTicket = ({ market, submit }: DealTicketProps) => {
|
||||
errors.summary?.type,
|
||||
]);
|
||||
|
||||
// When order state changes persist it in local storage
|
||||
useEffect(() => setPersistedOrder(order), [order, setPersistedOrder]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(order: OrderSubmissionBody['orderSubmission']) => {
|
||||
if (!pubKey) {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import produce from 'immer';
|
||||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
type OrderData = OrderSubmissionBody['orderSubmission'] | null;
|
||||
|
||||
type PersistedOrderStore = {
|
||||
orders: OrderData[];
|
||||
getOrder: (marketId: string) => OrderData | undefined;
|
||||
setOrder: (order: OrderData) => void;
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
export const usePersistedOrderStore = create(
|
||||
persist<PersistedOrderStore>(
|
||||
(set, get) => ({
|
||||
orders: [],
|
||||
getOrder: (marketId) => {
|
||||
const persisted = get().orders.find((o) => o?.marketId === marketId);
|
||||
return persisted;
|
||||
},
|
||||
setOrder: (order) => {
|
||||
set(
|
||||
produce((store: PersistedOrderStore) => {
|
||||
const persisted = store.orders.find(
|
||||
(o) => o?.marketId === order?.marketId
|
||||
);
|
||||
if (persisted) {
|
||||
Object.assign(persisted, order);
|
||||
} else {
|
||||
store.orders.push(order);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
clear: () => set({ orders: [] }),
|
||||
}),
|
||||
{
|
||||
name: 'VEGA_DEAL_TICKET_ORDER_STORE',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
Icon,
|
||||
Input,
|
||||
InputError,
|
||||
Select,
|
||||
RichSelect,
|
||||
Option,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
@@ -171,22 +172,29 @@ export const DepositForm = ({
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
id="asset"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
onSelectAsset(e.target.value);
|
||||
<RichSelect
|
||||
data-testid="select-asset"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
onSelectAsset(value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={selectedAsset?.id || ''}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={selectedAsset?.id}
|
||||
hasError={Boolean(errors.asset?.message)}
|
||||
>
|
||||
<option value="">{t('Please select')}</option>
|
||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
<Option key={a.id} value={a.id}>
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{a.name}</span>
|
||||
<span className="text-[10px] font-mono">
|
||||
<span className="text-gray-500">{a.id} -</span> {a.symbol}
|
||||
</span>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</RichSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.asset?.message && (
|
||||
|
||||
@@ -96,13 +96,18 @@ export const NetworkSwitcher = () => {
|
||||
const menuRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={handleOpen}>
|
||||
<DropdownMenuTrigger
|
||||
ref={menuRef}
|
||||
className="flex justify-between items-center"
|
||||
>
|
||||
{envTriggerMapping[VEGA_ENV]}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpen}
|
||||
trigger={
|
||||
<DropdownMenuTrigger
|
||||
ref={menuRef}
|
||||
className="flex justify-between items-center"
|
||||
>
|
||||
{envTriggerMapping[VEGA_ENV]}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
style={{ minWidth: `${menuRef.current?.offsetWidth || 290}px` }}
|
||||
|
||||
@@ -56,7 +56,6 @@ describe('market depth provider update', () => {
|
||||
];
|
||||
expect(update(data, delta.slice(0, 1), reload)).toBe(data);
|
||||
expect(update(data, delta.slice(1, 2), reload)).toBe(data);
|
||||
expect(mockCaptureException).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('restarts and captureException when there is gap in updates', () => {
|
||||
|
||||
@@ -24,11 +24,6 @@ export const update: Update<
|
||||
continue;
|
||||
}
|
||||
if (BigInt(delta.sequenceNumber) <= BigInt(data.depth.sequenceNumber)) {
|
||||
captureException(
|
||||
new Error(
|
||||
`Sequence number from delta is lower or equal to last sequenceNumber for ${data.id}, ${delta.sequenceNumber} <= ${data.depth.sequenceNumber}, update skipped`
|
||||
)
|
||||
);
|
||||
return data;
|
||||
}
|
||||
if (delta.previousSequenceNumber !== data.depth.sequenceNumber) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Orderbook } from './orderbook';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { addDecimal, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { marketDepthProvider } from './market-depth-provider';
|
||||
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
|
||||
import type { MarketData } from '@vegaprotocol/market-list';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
mapMarketData,
|
||||
} from './orderbook-data';
|
||||
import type { OrderbookData } from './orderbook-data';
|
||||
import { usePersistedOrderStore } from '@vegaprotocol/orders';
|
||||
|
||||
interface OrderbookManagerProps {
|
||||
marketId: string;
|
||||
@@ -122,7 +123,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
marketDataRef.current = marketData;
|
||||
|
||||
useEffect(() => {
|
||||
const throttleRunnner = updateOrderbookData.current;
|
||||
const throttleRunner = updateOrderbookData.current;
|
||||
if (!marketDataRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +140,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
setOrderbookData(dataRef.current);
|
||||
|
||||
return () => {
|
||||
throttleRunnner.cancel();
|
||||
throttleRunner.cancel();
|
||||
};
|
||||
}, [data, marketData, resolution]);
|
||||
|
||||
@@ -148,6 +149,8 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
flush();
|
||||
}, [resolution, flush]);
|
||||
|
||||
const updatePrice = usePersistedOrderStore((store) => store.updatePrice);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading || marketDataLoading || marketLoading}
|
||||
@@ -160,6 +163,12 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
|
||||
resolution={resolution}
|
||||
onResolutionChange={(resolution: number) => setResolution(resolution)}
|
||||
onClick={(price?: string | number) => {
|
||||
if (price) {
|
||||
const priceValue = addDecimal(price, market?.decimalPlaces ?? 0);
|
||||
updatePrice(marketId, priceValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ interface OrderbookRowProps {
|
||||
price: string;
|
||||
relativeAsk?: number;
|
||||
relativeBid?: number;
|
||||
onClick?: (price?: string | number) => void;
|
||||
}
|
||||
|
||||
export const OrderbookRow = React.memo(
|
||||
@@ -37,6 +38,7 @@ export const OrderbookRow = React.memo(
|
||||
price,
|
||||
relativeAsk,
|
||||
relativeBid,
|
||||
onClick,
|
||||
}: OrderbookRowProps) => {
|
||||
return (
|
||||
<>
|
||||
@@ -57,6 +59,7 @@ export const OrderbookRow = React.memo(
|
||||
<PriceCell
|
||||
testId={`price-${price}`}
|
||||
value={BigInt(price)}
|
||||
onClick={() => onClick && onClick(price)}
|
||||
valueFormatted={addDecimalsFormatNumber(price, decimalPlaces)}
|
||||
/>
|
||||
<CumulativeVol
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { OrderbookRow } from './orderbook-row';
|
||||
import { createRow, getPriceLevel } from './orderbook-data';
|
||||
import { Icon, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Checkbox, Icon, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import type { OrderbookData, OrderbookRowData } from './orderbook-data';
|
||||
|
||||
interface OrderbookProps extends OrderbookData {
|
||||
@@ -30,6 +30,7 @@ interface OrderbookProps extends OrderbookData {
|
||||
positionDecimalPlaces: number;
|
||||
resolution: number;
|
||||
onResolutionChange: (resolution: number) => void;
|
||||
onClick?: (price?: string | number) => void;
|
||||
fillGaps?: boolean;
|
||||
}
|
||||
|
||||
@@ -279,6 +280,7 @@ export const Orderbook = ({
|
||||
resolution,
|
||||
fillGaps: initialFillGaps,
|
||||
onResolutionChange,
|
||||
onClick,
|
||||
}: OrderbookProps) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
const scrollElement = useRef<HTMLDivElement>(null);
|
||||
@@ -533,6 +535,7 @@ export const Orderbook = ({
|
||||
<OrderbookRow
|
||||
key={data.price}
|
||||
price={(BigInt(data.price) / BigInt(resolution)).toString()}
|
||||
onClick={onClick}
|
||||
decimalPlaces={decimalPlaces - Math.log10(resolution)}
|
||||
positionDecimalPlaces={positionDecimalPlaces}
|
||||
bid={data.bid}
|
||||
@@ -635,19 +638,18 @@ export const Orderbook = ({
|
||||
</div>
|
||||
<div
|
||||
className="absolute bottom-0 grid grid-cols-4 gap-2 border-t-[1px] border-default mt-2 z-10 bg-white dark:bg-black w-full"
|
||||
style={{ gridAutoRows: '17px' }}
|
||||
style={{ gridAutoRows: '20px' }}
|
||||
ref={footerElement}
|
||||
>
|
||||
<div className="col-span-2">
|
||||
<label className="flex items-center whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<input
|
||||
className="mr-1"
|
||||
type="checkbox"
|
||||
checked={fillGaps}
|
||||
onChange={() => setFillGaps(!fillGaps)}
|
||||
/>
|
||||
{t('Show prices with no orders')}
|
||||
</label>
|
||||
<Checkbox
|
||||
name="empty-prices"
|
||||
checked={fillGaps}
|
||||
onCheckedChange={() => setFillGaps((curr) => !curr)}
|
||||
label={
|
||||
<span className="text-xs">{t('Show prices with no orders')}</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-start-3">
|
||||
<select
|
||||
|
||||
@@ -5,6 +5,7 @@ query MarketInfo($marketId: ID!, $interval: Interval!, $since: String!) {
|
||||
positionDecimalPlaces
|
||||
state
|
||||
tradingMode
|
||||
lpPriceRange
|
||||
proposal {
|
||||
id
|
||||
rationale {
|
||||
@@ -58,6 +59,7 @@ query MarketInfo($marketId: ID!, $interval: Interval!, $since: String!) {
|
||||
id
|
||||
}
|
||||
markPrice
|
||||
midPrice
|
||||
bestBidVolume
|
||||
bestOfferVolume
|
||||
bestStaticBidVolume
|
||||
|
||||
@@ -10,7 +10,7 @@ export type MarketInfoQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type MarketInfoQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any }, openingAuction: { __typename?: 'AuctionDuration', durationSecs: number, volume: number }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string } } } | null> | null } | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, priceMonitoringSettings: { __typename?: 'PriceMonitoringSettings', parameters?: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null } | null }, riskFactors?: { __typename?: 'RiskFactor', market: string, short: string, long: string } | null, data?: { __typename?: 'MarketData', markPrice: string, bestBidVolume: string, bestOfferVolume: string, bestStaticBidVolume: string, bestStaticOfferVolume: string, bestBidPrice: string, bestOfferPrice: string, trigger: Types.AuctionTrigger, openInterest: string, suppliedStake?: string | null, targetStake?: string | null, marketValueProxy: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: string, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', volume: string } } | null> | null } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } }, riskModel: { __typename?: 'LogNormalRiskModel', tau: number, riskAversionParameter: number, params: { __typename?: 'LogNormalModelParams', r: number, sigma: number, mu: number } } | { __typename?: 'SimpleRiskModel', params: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } } | null };
|
||||
export type MarketInfoQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, lpPriceRange: string, proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any }, openingAuction: { __typename?: 'AuctionDuration', durationSecs: number, volume: number }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string } } } | null> | null } | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, priceMonitoringSettings: { __typename?: 'PriceMonitoringSettings', parameters?: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null } | null }, riskFactors?: { __typename?: 'RiskFactor', market: string, short: string, long: string } | null, data?: { __typename?: 'MarketData', markPrice: string, midPrice: string, bestBidVolume: string, bestOfferVolume: string, bestStaticBidVolume: string, bestStaticOfferVolume: string, bestBidPrice: string, bestOfferPrice: string, trigger: Types.AuctionTrigger, openInterest: string, suppliedStake?: string | null, targetStake?: string | null, marketValueProxy: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: string, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', volume: string } } | null> | null } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } }, riskModel: { __typename?: 'LogNormalRiskModel', tau: number, riskAversionParameter: number, params: { __typename?: 'LogNormalModelParams', r: number, sigma: number, mu: number } } | { __typename?: 'SimpleRiskModel', params: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } } | null };
|
||||
|
||||
|
||||
export const MarketInfoDocument = gql`
|
||||
@@ -21,6 +21,7 @@ export const MarketInfoDocument = gql`
|
||||
positionDecimalPlaces
|
||||
state
|
||||
tradingMode
|
||||
lpPriceRange
|
||||
proposal {
|
||||
id
|
||||
rationale {
|
||||
@@ -74,6 +75,7 @@ export const MarketInfoDocument = gql`
|
||||
id
|
||||
}
|
||||
markPrice
|
||||
midPrice
|
||||
bestBidVolume
|
||||
bestOfferVolume
|
||||
bestStaticBidVolume
|
||||
|
||||
@@ -2,7 +2,9 @@ import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { totalFeesPercentage } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
removePaginationWrapper,
|
||||
t,
|
||||
useDataProvider,
|
||||
@@ -182,6 +184,9 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
state: Schema.MarketStateMapping[market.state],
|
||||
};
|
||||
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const marketSpecPanels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
@@ -195,9 +200,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
Schema.MarketTradingModeMapping[keyDetails.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces:
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.decimals,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@@ -295,10 +298,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
/>
|
||||
<MarketInfoTable
|
||||
data={{ referencePrice: trigger.referencePrice }}
|
||||
decimalPlaces={
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset
|
||||
.decimals
|
||||
}
|
||||
decimalPlaces={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
@@ -324,10 +324,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
suppliedStake: market.data && market.data?.suppliedStake,
|
||||
marketValueProxy: market.data && market.data.marketValueProxy,
|
||||
}}
|
||||
decimalPlaces={
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.decimals
|
||||
}
|
||||
decimalPlaces={assetDecimals}
|
||||
assetSymbol={assetSymbol}
|
||||
>
|
||||
<Link
|
||||
@@ -340,6 +337,36 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
</MarketInfoTable>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
liquidityPriceRange: formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
),
|
||||
LPVolumeMin:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
LPVolumeMax:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
@@ -369,7 +396,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
content: (
|
||||
<div className="">
|
||||
<ExternalLink
|
||||
className="mb-2"
|
||||
className="mb-2 w-full"
|
||||
href={generatePath(TokenLinks.PROPOSAL_PAGE, {
|
||||
tokenUrl: VEGA_TOKEN_URL,
|
||||
proposalId: market.proposal?.id || '',
|
||||
@@ -383,7 +410,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
{t('View governance proposal')}
|
||||
</ExternalLink>
|
||||
<ExternalLink
|
||||
className="mt-2"
|
||||
className="w-full"
|
||||
href={generatePath(TokenLinks.UPDATE_PROPOSAL_PAGE, {
|
||||
tokenUrl: VEGA_TOKEN_URL,
|
||||
})}
|
||||
|
||||
@@ -92,12 +92,14 @@ export const marketInfoQuery = (
|
||||
short: '0.008571790367285281',
|
||||
long: '0.008508132993273576',
|
||||
},
|
||||
lpPriceRange: '0.02',
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '54b78c1b877e106842ae156332ccec740ad98d6bad43143ac6a029501dd7c6e0',
|
||||
},
|
||||
midPrice: '5749',
|
||||
markPrice: '5749',
|
||||
suppliedStake: '56767',
|
||||
marketValueProxy: '677678',
|
||||
|
||||
@@ -98,4 +98,7 @@ export const tooltipMapping: Record<string, ReactNode> = {
|
||||
`The market's liquidity requirement which is derived from the maximum open interest observed over a rolling time window.`
|
||||
),
|
||||
suppliedStake: t('The current amount of liquidity supplied for this market.'),
|
||||
liquidityPriceRange: t(
|
||||
`Percentage move up and down from the mid price, which specifies the range of price levels over which automated liquidity provision orders will be deployed.`
|
||||
),
|
||||
};
|
||||
|
||||
@@ -245,7 +245,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
||||
colId="amend"
|
||||
headerName=""
|
||||
field="status"
|
||||
minWidth={150}
|
||||
minWidth={100}
|
||||
type="rightAligned"
|
||||
cellRenderer={({ data, node }: VegaICellRendererParams<Order>) => {
|
||||
return data && isOrderAmendable(data) ? (
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './use-order-cancel';
|
||||
export * from './use-order-submit';
|
||||
export * from './use-order-edit';
|
||||
export * from './use-order-event';
|
||||
export * from './use-persisted-order';
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import produce from 'immer';
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type OrderData = OrderSubmissionBody['orderSubmission'] | null;
|
||||
|
||||
type PersistedOrderStore = {
|
||||
orders: OrderData[];
|
||||
getOrder: (marketId: string) => OrderData | undefined;
|
||||
setOrder: (order: OrderData) => void;
|
||||
clear: () => void;
|
||||
updatePrice: (marketId: string, price: string) => void;
|
||||
};
|
||||
|
||||
export const usePersistedOrderStore = create<PersistedOrderStore>()(
|
||||
persist(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
orders: [],
|
||||
getOrder: (marketId: string) => {
|
||||
const current = get() as PersistedOrderStore;
|
||||
const persisted = current.orders.find((o) => o?.marketId === marketId);
|
||||
return persisted;
|
||||
},
|
||||
setOrder: (order: OrderData) => {
|
||||
set(
|
||||
produce((store: PersistedOrderStore) => {
|
||||
const persisted = store.orders.find(
|
||||
(o) => o?.marketId === order?.marketId
|
||||
);
|
||||
if (persisted) {
|
||||
if (!isEqual(persisted, order)) {
|
||||
Object.assign(persisted, order);
|
||||
} else {
|
||||
// NOOP
|
||||
}
|
||||
} else {
|
||||
store.orders.push(order);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
clear: () => set({ orders: [] }),
|
||||
updatePrice: (marketId: string, price: string) =>
|
||||
set(
|
||||
produce((store: PersistedOrderStore) => {
|
||||
const persisted = store.orders.find(
|
||||
(o) => o?.marketId === marketId
|
||||
);
|
||||
if (persisted) {
|
||||
persisted.price = price;
|
||||
}
|
||||
})
|
||||
),
|
||||
})),
|
||||
{
|
||||
name: 'VEGA_DEAL_TICKET_ORDER_STORE',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const usePersistedOrderStoreSubscription = (
|
||||
marketId: string,
|
||||
onOrderChange: (order: NonNullable<OrderData>) => void
|
||||
) => {
|
||||
const selector = (state: PersistedOrderStore) =>
|
||||
state.orders.find((o) => o?.marketId === marketId);
|
||||
const action = (storedOrder: OrderData | undefined) => {
|
||||
if (storedOrder) {
|
||||
onOrderChange(storedOrder);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = usePersistedOrderStore.subscribe(selector, action);
|
||||
useEffect(() => () => unsubscribe(), [unsubscribe]);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { marginsDataProvider } from './margin-data-provider';
|
||||
@@ -22,19 +22,16 @@ const getMarketMarginPosition = ({
|
||||
export const useMarketMargin = (marketId: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [marginLevel, setMarginLevel] = useState<string>('');
|
||||
const variables = useMemo(() => {
|
||||
return { partyId: pubKey || '' };
|
||||
}, [pubKey]);
|
||||
|
||||
const update = useCallback(
|
||||
({ data }: { data: MarginsQuery['party'] | null }) => {
|
||||
const marginMarketPosition = getMarketMarginPosition({ data, marketId });
|
||||
if (marginLevel !== marginMarketPosition?.maintenanceLevel) {
|
||||
if (marginMarketPosition?.maintenanceLevel) {
|
||||
setMarginLevel(marginMarketPosition?.maintenanceLevel || '');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[marginLevel, setMarginLevel, marketId]
|
||||
[setMarginLevel, marketId]
|
||||
);
|
||||
|
||||
useDataProvider<
|
||||
@@ -42,7 +39,7 @@ export const useMarketMargin = (marketId: string) => {
|
||||
MarginsSubscriptionSubscription['margins']
|
||||
>({
|
||||
dataProvider: marginsDataProvider,
|
||||
variables,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey || !marketId,
|
||||
update,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { positionsDataProvider } from './positions-data-providers';
|
||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
@@ -22,18 +22,15 @@ const getMarketPosition = ({
|
||||
export const useMarketPositionOpenVolume = (marketId: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [openVolume, setOpenVolume] = useState<string>('');
|
||||
const variables = useMemo(() => {
|
||||
return { partyId: pubKey || '' };
|
||||
}, [pubKey]);
|
||||
const update = useCallback(
|
||||
({ data }: { data: PositionsQuery['party'] | undefined }) => {
|
||||
const position = getMarketPosition({ data, marketId });
|
||||
if (openVolume !== position?.openVolume) {
|
||||
if (position?.openVolume) {
|
||||
setOpenVolume(position?.openVolume || '');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[openVolume, setOpenVolume, marketId]
|
||||
[setOpenVolume, marketId]
|
||||
);
|
||||
|
||||
useDataProvider<
|
||||
@@ -41,7 +38,7 @@ export const useMarketPositionOpenVolume = (marketId: string) => {
|
||||
PositionsSubscriptionSubscription['positions']
|
||||
>({
|
||||
dataProvider: positionsDataProvider,
|
||||
variables,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey || !marketId,
|
||||
update,
|
||||
});
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './use-theme-switcher';
|
||||
export * from './use-storybook-theme-observer';
|
||||
export * from './use-yesterday';
|
||||
export * from './use-previous';
|
||||
export * from './use-logger';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useRef } from 'react';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import type { LocalLogger } from '../lib/local-logger';
|
||||
import { localLoggerFactory } from '../lib/local-logger';
|
||||
|
||||
interface Props {
|
||||
dsn?: string;
|
||||
application?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export const useLogger = ({ dsn, ...props }: Props) => {
|
||||
const logger = useRef<LocalLogger | null>(null);
|
||||
if (!logger.current) {
|
||||
logger.current = localLoggerFactory(props);
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
dsn,
|
||||
integrations: [new BrowserTracing()],
|
||||
tracesSampleRate: 1,
|
||||
defaultIntegrations: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
return logger.current;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import { LocalStorage } from '../lib/storage';
|
||||
|
||||
const THEME_STORAGE_KEY = 'theme';
|
||||
|
||||
@@ -16,3 +16,4 @@ export * from './lib/is-asset-erc20';
|
||||
export * from './lib/remove-pagination-wrapper';
|
||||
export * from './lib/__generated__/ChainId';
|
||||
export * from './lib/data-grid';
|
||||
export * from './lib/local-logger';
|
||||
|
||||
@@ -4,11 +4,12 @@ export interface IPriceCellProps {
|
||||
value: number | bigint | null | undefined;
|
||||
valueFormatted: string;
|
||||
testId?: string;
|
||||
onClick?: (price?: string | number) => void;
|
||||
}
|
||||
|
||||
export const PriceCell = memo(
|
||||
forwardRef<HTMLSpanElement, IPriceCellProps>(
|
||||
({ value, valueFormatted, testId }: IPriceCellProps, ref) => {
|
||||
({ value, valueFormatted, testId, onClick }: IPriceCellProps, ref) => {
|
||||
if (!isNumeric(value)) {
|
||||
return (
|
||||
<span data-testid="price" ref={ref}>
|
||||
@@ -20,7 +21,25 @@ export const PriceCell = memo(
|
||||
const valueSplit: string[] = decimalSeparator
|
||||
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
|
||||
: [`${value}`];
|
||||
return (
|
||||
return onClick ? (
|
||||
<button
|
||||
onClick={() => onClick(value)}
|
||||
className="hover:dark:bg-neutral-800 hover:bg-neutral-200"
|
||||
>
|
||||
<span
|
||||
ref={ref}
|
||||
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
|
||||
data-testid={testId || 'price'}
|
||||
title={valueFormatted}
|
||||
>
|
||||
{valueSplit[0]}
|
||||
{valueSplit[1] ? decimalSeparator : null}
|
||||
{valueSplit[1] ? (
|
||||
<span className="opacity-60">{valueSplit[1]}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
ref={ref}
|
||||
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './time';
|
||||
export * from './links';
|
||||
export * from './remove-pagination-wrapper';
|
||||
export * from './data-grid';
|
||||
export * from './local-logger';
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import type { Severity } from '@sentry/types/types/severity';
|
||||
import { LocalLogger, localLoggerFactory } from './local-logger';
|
||||
|
||||
const methods = [
|
||||
'debug',
|
||||
'info',
|
||||
'log',
|
||||
'warn',
|
||||
'error',
|
||||
'critical',
|
||||
'fatal',
|
||||
] as const;
|
||||
const methodToConsoleMethod = [
|
||||
'debug',
|
||||
'info',
|
||||
'log',
|
||||
'warn',
|
||||
'error',
|
||||
'error',
|
||||
'error',
|
||||
] as const;
|
||||
|
||||
describe('LocalLogger', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('logger should be properly instantiate', () => {
|
||||
const logger = localLoggerFactory({});
|
||||
expect(logger).toBeInstanceOf(LocalLogger);
|
||||
methods.forEach((method) => {
|
||||
expect(logger[method]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('each method should call console', () => {
|
||||
const methodToLevel = [
|
||||
'debug',
|
||||
'info',
|
||||
'log',
|
||||
'warning',
|
||||
'error',
|
||||
'critical',
|
||||
'fatal',
|
||||
];
|
||||
const logger = localLoggerFactory({ logLevel: 'debug' });
|
||||
methods.forEach((method, i) => {
|
||||
const consoleMethod = methodToConsoleMethod[i];
|
||||
jest.spyOn(console, consoleMethod).mockImplementation();
|
||||
logger[method]('test', 'test2');
|
||||
expect(console[consoleMethod]).toHaveBeenCalledWith(
|
||||
`trading:${methodToLevel[i]}: `,
|
||||
'test',
|
||||
'test2'
|
||||
);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
it('each method should call sentry', () => {
|
||||
jest.spyOn(Sentry, 'captureMessage');
|
||||
jest.spyOn(Sentry, 'captureException');
|
||||
const logger = localLoggerFactory({ logLevel: 'debug' });
|
||||
methods.forEach((method, i) => {
|
||||
jest.spyOn(console, methodToConsoleMethod[i]).mockImplementation();
|
||||
logger[method]('test', 'test2');
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (i < 4) {
|
||||
expect(Sentry.captureMessage).toHaveBeenCalled();
|
||||
expect(Sentry.captureException).not.toHaveBeenCalled();
|
||||
} else {
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled();
|
||||
expect(Sentry.captureException).toHaveBeenCalled();
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
}
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
it('breadcrumb method should call Sentry', () => {
|
||||
const logger = localLoggerFactory({});
|
||||
jest.spyOn(Sentry, 'addBreadcrumb');
|
||||
const breadCrumb = {
|
||||
type: 'type',
|
||||
level: 'fatal' as Severity,
|
||||
event_id: 'event_id',
|
||||
category: 'category',
|
||||
message: 'message',
|
||||
data: {
|
||||
data_1: 'data_1',
|
||||
data_2: 'data_2',
|
||||
},
|
||||
timestamp: 1111111,
|
||||
};
|
||||
logger.addSentryBreadcrumb(breadCrumb);
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith(breadCrumb);
|
||||
});
|
||||
|
||||
it('setLogLevel should change log level', () => {
|
||||
const logger = localLoggerFactory({ logLevel: 'info' });
|
||||
jest.spyOn(console, 'debug').mockImplementation();
|
||||
logger.debug('test', 'test1');
|
||||
expect(console.debug).not.toHaveBeenCalled();
|
||||
|
||||
logger.setLogLevel('debug');
|
||||
logger.debug('test', 'test1');
|
||||
expect(console.debug).toHaveBeenCalledWith(
|
||||
'trading:debug: ',
|
||||
'test',
|
||||
'test1'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import type { Scope } from '@sentry/browser';
|
||||
import type { Severity, Breadcrumb, Primitive } from '@sentry/types';
|
||||
|
||||
const LogLevels = [
|
||||
'fatal',
|
||||
'error',
|
||||
'warning',
|
||||
'log',
|
||||
'info',
|
||||
'debug',
|
||||
'critical',
|
||||
'silent',
|
||||
];
|
||||
type LogLevelsType = typeof LogLevels[number];
|
||||
type ConsoleArg = string | number | boolean | bigint | symbol | object;
|
||||
type ConsoleMethod = {
|
||||
[K in keyof Console]: Console[K] extends (...args: ConsoleArg[]) => unknown
|
||||
? K
|
||||
: never;
|
||||
}[keyof Console] &
|
||||
string;
|
||||
|
||||
interface LoggerConf {
|
||||
application?: string;
|
||||
tags?: string[];
|
||||
logLevel?: LogLevelsType;
|
||||
}
|
||||
|
||||
const isPrimitive = (arg: ConsoleArg | undefined | null): arg is Primitive => {
|
||||
return ['string', 'number', 'boolean', 'bigint', 'symbol'].includes(
|
||||
typeof arg
|
||||
);
|
||||
};
|
||||
|
||||
export class LocalLogger {
|
||||
static levelLogMap: Record<LogLevelsType, number> = {
|
||||
debug: 10,
|
||||
info: 20,
|
||||
log: 30,
|
||||
warning: 40,
|
||||
error: 50,
|
||||
critical: 60,
|
||||
fatal: 70,
|
||||
silent: 80,
|
||||
};
|
||||
private _logLevel: LogLevelsType = 'info';
|
||||
private get numberLogLevel() {
|
||||
return LocalLogger.levelLogMap[this._logLevel];
|
||||
}
|
||||
private tags: string[] = [];
|
||||
private application = 'trading';
|
||||
constructor(conf: LoggerConf) {
|
||||
if (conf.application) {
|
||||
this.application = conf.application;
|
||||
}
|
||||
this.tags = [...(conf.tags || [])];
|
||||
this._logLevel = conf.logLevel || this._logLevel;
|
||||
}
|
||||
public debug(...args: ConsoleArg[]) {
|
||||
this._log('debug', 'debug', args);
|
||||
}
|
||||
public info(...args: ConsoleArg[]) {
|
||||
this._log('info', 'info', args);
|
||||
}
|
||||
public log(...args: ConsoleArg[]) {
|
||||
this._log('log', 'log', args);
|
||||
}
|
||||
public warn(...args: ConsoleArg[]) {
|
||||
this._log('warning', 'warn', args);
|
||||
}
|
||||
public error(...args: ConsoleArg[]) {
|
||||
this._log('error', 'error', args);
|
||||
}
|
||||
public critical(...args: ConsoleArg[]) {
|
||||
this._log('critical', 'error', args);
|
||||
}
|
||||
public fatal(...args: ConsoleArg[]) {
|
||||
this._log('fatal', 'error', args);
|
||||
}
|
||||
private _log(
|
||||
level: LogLevelsType,
|
||||
logMethod: ConsoleMethod,
|
||||
args: ConsoleArg[]
|
||||
) {
|
||||
if (this.numberLogLevel <= LocalLogger.levelLogMap[level]) {
|
||||
console[logMethod].apply(console, [
|
||||
`${this.application}:${level}: `,
|
||||
...args,
|
||||
]);
|
||||
}
|
||||
this._transmit(level, args);
|
||||
}
|
||||
private _extractArgs(
|
||||
level: LogLevelsType,
|
||||
args: ConsoleArg[]
|
||||
): [string, Error, Scope] {
|
||||
const arg = args.shift();
|
||||
const error = arg instanceof Error ? arg : null;
|
||||
const msg = error ? error.message : String(arg);
|
||||
const scope = new Sentry.Scope();
|
||||
scope.setLevel(level as Severity);
|
||||
let logArgs: Record<string, unknown>;
|
||||
try {
|
||||
logArgs = { args: JSON.stringify(args) };
|
||||
} catch (e) {
|
||||
logArgs = { args };
|
||||
}
|
||||
scope.setContext('event-record', logArgs);
|
||||
if (this.tags.length) {
|
||||
this.tags.forEach((tag) => {
|
||||
const found = args.reduce((aggr, arg) => {
|
||||
if (typeof arg === 'object' && tag in arg) {
|
||||
// @ts-ignore change object to record
|
||||
aggr = arg[tag] as unknown as Primitive | object;
|
||||
}
|
||||
return aggr;
|
||||
}, null as Primitive | object);
|
||||
if (isPrimitive(found)) {
|
||||
scope.setTag(tag, found);
|
||||
}
|
||||
});
|
||||
}
|
||||
return [msg, error || new Error(msg), scope];
|
||||
}
|
||||
private _transmit(level: LogLevelsType, args: ConsoleArg[]) {
|
||||
const [msg, error, logEvent] = this._extractArgs(level, args);
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
case 'info':
|
||||
case 'log':
|
||||
case 'warning':
|
||||
Sentry.captureMessage(msg, logEvent);
|
||||
return;
|
||||
case 'error':
|
||||
case 'critical':
|
||||
case 'fatal':
|
||||
Sentry.captureException(error, logEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
public addSentryBreadcrumb(breadcrumb: Breadcrumb) {
|
||||
Sentry.addBreadcrumb(breadcrumb);
|
||||
}
|
||||
public setLogLevel(logLevel: LogLevelsType) {
|
||||
this._logLevel = logLevel;
|
||||
}
|
||||
public get logLevel() {
|
||||
return this._logLevel;
|
||||
}
|
||||
}
|
||||
|
||||
let singleLoggerInstance: LocalLogger;
|
||||
|
||||
export const localLoggerFactory = (conf: LoggerConf) => {
|
||||
if (!singleLoggerInstance) {
|
||||
singleLoggerInstance = new LocalLogger(conf);
|
||||
}
|
||||
if (conf.logLevel && singleLoggerInstance.logLevel !== conf.logLevel) {
|
||||
singleLoggerInstance.setLogLevel(conf.logLevel);
|
||||
}
|
||||
return singleLoggerInstance;
|
||||
};
|
||||
@@ -89,4 +89,7 @@ module.exports = {
|
||||
'rotate-back': 'rotate 2s linear reverse infinite',
|
||||
wave: 'wave 2s linear infinite',
|
||||
},
|
||||
data: {
|
||||
selected: 'state~="checked"',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MAX_TRADES, tradesWithMarketProvider } from './trades-data-provider';
|
||||
import { TradesTable } from './trades-table';
|
||||
import type { Trade, TradeEdge } from './trades-data-provider';
|
||||
import type { TradesQueryVariables } from './__generated__/Trades';
|
||||
import { usePersistedOrderStore } from '@vegaprotocol/orders';
|
||||
|
||||
interface TradesContainerProps {
|
||||
marketId: string;
|
||||
@@ -21,6 +22,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
const totalCountRef = useRef<number | undefined>(undefined);
|
||||
const newRows = useRef(0);
|
||||
const scrolledToTop = useRef(true);
|
||||
const updatePrice = usePersistedOrderStore((store) => store.updatePrice);
|
||||
|
||||
const variables = useMemo<TradesQueryVariables>(
|
||||
() => ({ marketId, maxTrades: MAX_TRADES }),
|
||||
@@ -113,6 +115,11 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||
datasource={{ getRows }}
|
||||
onBodyScrollEnd={onBodyScrollEnd}
|
||||
onBodyScroll={onBodyScroll}
|
||||
onClick={(price?: string) => {
|
||||
if (price) {
|
||||
updatePrice(marketId, price);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimal,
|
||||
@@ -49,6 +50,7 @@ export interface Datasource extends IDatasource {
|
||||
interface Props extends AgGridReactProps {
|
||||
rowData?: Trade[] | null;
|
||||
datasource?: Datasource;
|
||||
onClick?: (price?: string) => void;
|
||||
}
|
||||
|
||||
type TradesTableValueFormatterParams = Omit<
|
||||
@@ -87,6 +89,27 @@ export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||
}
|
||||
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
|
||||
}}
|
||||
cellRenderer={({
|
||||
value,
|
||||
data,
|
||||
}: VegaICellRendererParams<Trade, 'price'>) => {
|
||||
if (!data?.market || !value) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
props.onClick &&
|
||||
props.onClick(
|
||||
addDecimal(value, data.market?.decimalPlaces || 0)
|
||||
)
|
||||
}
|
||||
className="hover:dark:bg-neutral-800 hover:bg-neutral-200"
|
||||
>
|
||||
{addDecimalsFormatNumber(value, data.market.decimalPlaces)}
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Size')}
|
||||
|
||||
Generated
+5
-9
@@ -496,7 +496,7 @@ export type CoreSnapshotData = {
|
||||
/** Edge type containing the core snapshot cursor information */
|
||||
export type CoreSnapshotEdge = {
|
||||
__typename?: 'CoreSnapshotEdge';
|
||||
/** Cursor identifying the core snapashot data */
|
||||
/** Cursor identifying the core snapshot data */
|
||||
cursor: Scalars['String'];
|
||||
/** The core snapshot data */
|
||||
node: CoreSnapshotData;
|
||||
@@ -576,12 +576,12 @@ export type DataSourceSpec = {
|
||||
export type DataSourceSpecConfiguration = {
|
||||
__typename?: 'DataSourceSpecConfiguration';
|
||||
/**
|
||||
* filters describes which source data are considered of interest or not for
|
||||
* Filters describes which source data are considered of interest or not for
|
||||
* the product (or the risk model).
|
||||
*/
|
||||
filters?: Maybe<Array<Filter>>;
|
||||
/**
|
||||
* signers is the list of authorized signatures that signed the data for this
|
||||
* Signers is the list of authorized signatures that signed the data for this
|
||||
* data source. All the public keys in the data should be contained in this
|
||||
* list.
|
||||
*/
|
||||
@@ -1072,7 +1072,7 @@ export type Future = {
|
||||
dataSourceSpecBinding: DataSourceSpecToFutureBinding;
|
||||
/** The data source specification that describes the data of interest for settlement. */
|
||||
dataSourceSpecForSettlementData: DataSourceSpec;
|
||||
/** The data source specification describing the data source data of interest for trading termination. */
|
||||
/** The data source specification that describes the data-source data of interest for trading termination. */
|
||||
dataSourceSpecForTradingTermination: DataSourceSpec;
|
||||
/** String representing the quote (e.g. BTCUSD -> USD is quote) */
|
||||
quoteName: Scalars['String'];
|
||||
@@ -1796,10 +1796,6 @@ export enum MarketTradingMode {
|
||||
/** Information about whether proposals are enabled, if the markets are still bootstrapping, etc.. */
|
||||
export type NetworkLimits = {
|
||||
__typename?: 'NetworkLimits';
|
||||
/** How many blocks before the chain comes out of bootstrap mode */
|
||||
bootstrapBlockCount: Scalars['Int'];
|
||||
/** True once block count > bootstrapBlockCount */
|
||||
bootstrapFinished: Scalars['Boolean'];
|
||||
/** Are asset proposals allowed at this point in time */
|
||||
canProposeAsset: Scalars['Boolean'];
|
||||
/** Are market proposals allowed at this point in time */
|
||||
@@ -2960,7 +2956,7 @@ export enum ProposalRejectionReason {
|
||||
PROPOSAL_ERROR_ENACT_TIME_TOO_LATE = 'PROPOSAL_ERROR_ENACT_TIME_TOO_LATE',
|
||||
/** The specified enactment time is too early based on network parameters */
|
||||
PROPOSAL_ERROR_ENACT_TIME_TOO_SOON = 'PROPOSAL_ERROR_ENACT_TIME_TOO_SOON',
|
||||
/** The erc20 address specified by this proposal is alread in use by another asset */
|
||||
/** The ERC-20 address specified by this proposal is already in use by another asset */
|
||||
PROPOSAL_ERROR_ERC20_ADDRESS_ALREADY_IN_USE = 'PROPOSAL_ERROR_ERC20_ADDRESS_ALREADY_IN_USE',
|
||||
/** Proposal terms timestamps are not compatible (Validation < Closing < Enactment) */
|
||||
PROPOSAL_ERROR_INCOMPATIBLE_TIMESTAMPS = 'PROPOSAL_ERROR_INCOMPATIBLE_TIMESTAMPS',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Icon } from '../icon';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type CheckedState = boolean | 'indeterminate';
|
||||
export interface CheckboxProps {
|
||||
checked?: CheckedState;
|
||||
label?: string;
|
||||
label?: ReactNode;
|
||||
name?: string;
|
||||
onCheckedChange?: (checked: CheckedState) => void;
|
||||
disabled?: boolean;
|
||||
@@ -29,7 +30,7 @@ export const Checkbox = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex gap-1 items-center">
|
||||
<CheckboxPrimitive.Root
|
||||
name={name}
|
||||
id={name}
|
||||
@@ -47,7 +48,7 @@ export const Checkbox = ({
|
||||
) : (
|
||||
<Icon
|
||||
name="tick"
|
||||
className="relative w-[11px] h-[11px] text-white dark:text-black"
|
||||
className="relative w-[10px] h-[10px] text-white dark:text-black"
|
||||
/>
|
||||
)}
|
||||
</CheckboxPrimitive.CheckboxIndicator>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from './dropdown-menu';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('DropdownMenu', () => {
|
||||
const text = 'Dropdown menu content';
|
||||
|
||||
// Upgrade from @radix-ui/react-dropdown-menu 0.1.6 to 2.0.2 renders
|
||||
// dropdowns inline (rather than portals). Currently not using a portal
|
||||
// will break the UI due to z-index issues
|
||||
it('renders using a portal', async () => {
|
||||
render(
|
||||
<div className="test-wrapper">
|
||||
<DropdownMenu
|
||||
trigger={<DropdownMenuTrigger>Trigger</DropdownMenuTrigger>}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<p>{text}</p>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByText(/trigger/i));
|
||||
const contentElement = await screen.findByText(text);
|
||||
expect(contentElement).toBeInTheDocument();
|
||||
// if content is within .test-wrapper then its not been rendered in a portal
|
||||
expect(contentElement.closest('.test-wrapper')).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -29,16 +29,21 @@ export const CheckboxItems = () => {
|
||||
console.log(checkboxItems);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<span>Select many things</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
<span>Select many things</span>
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{checkboxItems.map(({ label, state: [checked, setChecked] }) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={label}
|
||||
checked={checked}
|
||||
onCheckedChange={setChecked}
|
||||
onCheckedChange={(checked) =>
|
||||
setChecked(typeof checked === 'boolean' ? checked : false)
|
||||
}
|
||||
>
|
||||
{label}
|
||||
<DropdownMenuItemIndicator />
|
||||
@@ -55,10 +60,13 @@ export const RadioItems = () => {
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 50 }}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<span>Open</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
<span>Open</span>
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onSelect={() => console.log('minimize')}>
|
||||
Minimize window
|
||||
@@ -92,10 +100,13 @@ export const IconMenu = () => {
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 50 }}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Icon name="cog" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger>
|
||||
<Icon name="cog" />
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{iconMenuItems.map(({ label }) => (
|
||||
<DropdownMenuItem key={label}>{label}</DropdownMenuItem>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { Icon } from '../icon';
|
||||
|
||||
@@ -12,11 +13,24 @@ const itemClass = classNames(
|
||||
'whitespace-nowrap'
|
||||
);
|
||||
|
||||
type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps & {
|
||||
trigger: ReactNode;
|
||||
};
|
||||
/**
|
||||
* Contains all the parts of a dropdown menu.
|
||||
*/
|
||||
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
export const DropdownMenu = ({
|
||||
children,
|
||||
trigger,
|
||||
...props
|
||||
}: DropdownMenuProps) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Root {...props}>
|
||||
{trigger}
|
||||
<DropdownMenuPrimitive.Portal>{children}</DropdownMenuPrimitive.Portal>
|
||||
</DropdownMenuPrimitive.Root>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* The button that toggles the dropdown menu.
|
||||
* By default, the {@link DropdownMenuContent} will position itself against the trigger.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { Select } from './select';
|
||||
import { RichSelect, Select, Option } from './select';
|
||||
|
||||
describe('Select', () => {
|
||||
it('should render successfully', () => {
|
||||
@@ -8,3 +7,31 @@ describe('Select', () => {
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RichSelect', () => {
|
||||
it('should render select element with placeholder when no value is pre-selected', async () => {
|
||||
const { findByTestId } = render(
|
||||
<RichSelect placeholder={'Select'}>
|
||||
<Option value={'1'}>1</Option>
|
||||
<Option value={'2'}>2</Option>
|
||||
</RichSelect>
|
||||
);
|
||||
const btn = (await findByTestId(
|
||||
'rich-select-trigger'
|
||||
)) as HTMLButtonElement;
|
||||
expect(btn.textContent).toEqual('Select');
|
||||
});
|
||||
|
||||
it('should render select element with pre-selected value', async () => {
|
||||
const { findByTestId } = render(
|
||||
<RichSelect placeholder={'Select'} value={'1'}>
|
||||
<Option value={'1'}>1</Option>
|
||||
<Option value={'2'}>2</Option>
|
||||
</RichSelect>
|
||||
);
|
||||
const btn = (await findByTestId(
|
||||
'rich-select-trigger'
|
||||
)) as HTMLButtonElement;
|
||||
expect(btn.textContent).toEqual('1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { Select } from './select';
|
||||
import { Option, Select, RichSelect } from './select';
|
||||
import { FormGroup } from '../form-group';
|
||||
|
||||
export default {
|
||||
@@ -17,6 +17,12 @@ const Template: Story = (args) => (
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
const RichSelectTemplate: Story = ({ placeholder, ...props }) => (
|
||||
<FormGroup label="Select an option" labelFor={props.id}>
|
||||
<RichSelect placeholder={placeholder} {...props} />
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
id: 'select-default',
|
||||
@@ -33,3 +39,53 @@ Disabled.args = {
|
||||
id: 'select-disabled',
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const RichDefaultSelect = RichSelectTemplate.bind({});
|
||||
RichDefaultSelect.args = {
|
||||
id: 'rich',
|
||||
name: 'rich',
|
||||
placeholder: 'Select an option',
|
||||
onValueChange: (v: string) => {
|
||||
console.log(v);
|
||||
},
|
||||
children: (
|
||||
<>
|
||||
<Option value="1">
|
||||
<div className="flex flex-col justify-start items-start">
|
||||
<span>Option One</span>
|
||||
<span className="text-xs">First option</span>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="2">
|
||||
<div className="flex flex-col justify-start items-start">
|
||||
<span>Option Two</span>
|
||||
<span className="text-xs">Second option</span>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="3">
|
||||
<div className="flex flex-col justify-start items-start">
|
||||
<span>Option Three</span>
|
||||
<span className="text-xs">Third option</span>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="4">
|
||||
<div className="flex flex-col justify-start items-start">
|
||||
<span>Option Four</span>
|
||||
<span className="text-xs">Fourth option</span>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="5">
|
||||
<div className="flex flex-col justify-start items-start">
|
||||
<span>Option Five</span>
|
||||
<span className="text-xs">Fifth option</span>
|
||||
</div>
|
||||
</Option>
|
||||
<Option value="6">
|
||||
<div className="flex flex-col justify-start items-start">
|
||||
<span>Option Six</span>
|
||||
<span className="text-xs">Sixth option</span>
|
||||
</div>
|
||||
</Option>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { SelectHTMLAttributes } from 'react';
|
||||
import type { Ref, SelectHTMLAttributes } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Icon } from '..';
|
||||
import { defaultSelectElement } from '../../utils/shared';
|
||||
import * as SelectPrimitive from '@radix-ui/react-select';
|
||||
|
||||
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
hasError?: boolean;
|
||||
@@ -27,3 +29,108 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export type RichSelectProps = React.ComponentProps<
|
||||
typeof SelectPrimitive.Root
|
||||
> & {
|
||||
placeholder: string;
|
||||
hasError?: boolean;
|
||||
id?: string;
|
||||
'data-testid'?: string;
|
||||
};
|
||||
export const RichSelect = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
RichSelectProps
|
||||
>(({ id, children, placeholder, hasError, ...props }, forwardedRef) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const contentRef = useRef<HTMLDivElement>();
|
||||
|
||||
const setWidth = () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.style.width = containerRef.current
|
||||
? `${containerRef.current.getBoundingClientRect().width}px`
|
||||
: 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef as Ref<HTMLDivElement>}
|
||||
className="flex items-center relative"
|
||||
>
|
||||
<SelectPrimitive.Root
|
||||
{...props}
|
||||
onOpenChange={() => {
|
||||
setWidth();
|
||||
}}
|
||||
defaultOpen={false}
|
||||
>
|
||||
<SelectPrimitive.Trigger
|
||||
data-testid={props['data-testid'] || 'rich-select-trigger'}
|
||||
className={classNames(
|
||||
defaultSelectElement(hasError),
|
||||
'rounded-md pl-2 pr-11'
|
||||
)}
|
||||
id={id}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<SelectPrimitive.Value placeholder={placeholder} />
|
||||
<SelectPrimitive.Icon className={classNames('absolute right-4')}>
|
||||
<Icon name="chevron-down" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
<SelectPrimitive.Portal container={containerRef.current}>
|
||||
<SelectPrimitive.Content
|
||||
ref={contentRef as Ref<HTMLDivElement>}
|
||||
className={classNames(
|
||||
'z-20',
|
||||
'bg-white dark:bg-black',
|
||||
'border border-neutral-500 focus:border-black dark:focus:border-white',
|
||||
'overflow-hidden'
|
||||
)}
|
||||
position={'popper'}
|
||||
side={'bottom'}
|
||||
align={'center'}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton className="flex items-center justify-center p-1 bg-neutral-50 dark:bg-neutral-900">
|
||||
<Icon name="chevron-up" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton className="flex items-center justify-center p-1 bg-neutral-50 dark:bg-neutral-900">
|
||||
<Icon name="chevron-down" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
</SelectPrimitive.Root>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const Option = forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentProps<typeof SelectPrimitive.Item>
|
||||
>(({ children, className, ...props }, forwardedRef) => (
|
||||
<SelectPrimitive.Item
|
||||
data-testid="rich-select-option"
|
||||
className={classNames(
|
||||
'relative',
|
||||
'text-black dark:text-white',
|
||||
'cursor-pointer outline-none',
|
||||
'hover:bg-neutral-100 dark:hover:bg-neutral-800',
|
||||
'focus:bg-neutral-100 dark:focus:bg-neutral-800',
|
||||
'pl-2 py-2',
|
||||
'pr-12',
|
||||
'w-full',
|
||||
'text-sm',
|
||||
'data-selected:bg-vega-yellow dark:data-selected:text-black dark:data-selected:bg-vega-yellow',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator className="absolute right-4 top-[50%] translate-y-[-50%]">
|
||||
<Icon name="tick" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
|
||||
@@ -115,6 +115,7 @@ export const Toast = ({
|
||||
>
|
||||
<div className="flex relative">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="toast-close"
|
||||
onClick={closeToast}
|
||||
className="absolute p-2 top-0 right-0"
|
||||
@@ -133,7 +134,7 @@ export const Toast = ({
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex-1 p-2 pr-6 text-sm overflow-auto"
|
||||
className="flex-1 p-2 pr-6 text-sm overflow-auto dark:bg-black dark:text-white"
|
||||
data-testid="toast-content"
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -7,7 +7,7 @@ import random from 'lodash/random';
|
||||
import sample from 'lodash/sample';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { useToasts } from './use-toasts';
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import { useEffect } from '@storybook/addons';
|
||||
import { formatNumber } from '@vegaprotocol/react-helpers';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import type { Toast } from './toast';
|
||||
|
||||
type ToastsStore = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
const actualCreate = jest.requireActual('zustand').default; // if using jest
|
||||
const zu = jest.requireActual('zustand'); // if using jest
|
||||
|
||||
// a variable to hold reset functions for all stores declared in the app
|
||||
const storeResetFns = new Set();
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
const create = (createState) => {
|
||||
const store = actualCreate(createState);
|
||||
export const create = (createState) => {
|
||||
const store = zu.create(createState);
|
||||
const initialState = store.getState();
|
||||
storeResetFns.add(() => store.setState(initialState, true));
|
||||
return store;
|
||||
@@ -16,5 +16,3 @@ const create = (createState) => {
|
||||
beforeEach(() => {
|
||||
act(() => storeResetFns.forEach((resetFn) => resetFn()));
|
||||
});
|
||||
|
||||
export default create;
|
||||
|
||||
@@ -23,10 +23,13 @@ import { ChainIdDocument } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const mockUpdateDialogOpen = jest.fn();
|
||||
const mockCloseVegaDialog = jest.fn();
|
||||
jest.mock('zustand', () => () => () => ({
|
||||
updateVegaWalletDialog: mockUpdateDialogOpen,
|
||||
closeVegaWalletDialog: mockCloseVegaDialog,
|
||||
vegaWalletDialogOpen: true,
|
||||
|
||||
jest.mock('zustand', () => ({
|
||||
create: () => () => ({
|
||||
updateVegaWalletDialog: mockUpdateDialogOpen,
|
||||
closeVegaWalletDialog: mockCloseVegaDialog,
|
||||
vegaWalletDialogOpen: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
let defaultProps: VegaConnectDialogProps;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from './connectors';
|
||||
import { determineId } from './utils';
|
||||
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import type { VegaTxState } from './use-vega-transaction';
|
||||
import { VegaTxStatus } from './use-vega-transaction';
|
||||
import type {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { act } from 'react-dom/test-utils';
|
||||
const actualCreate = jest.requireActual('zustand').default; // if using jest
|
||||
const zu = jest.requireActual('zustand'); // if using jest
|
||||
|
||||
// a variable to hold reset functions for all stores declared in the app
|
||||
const storeResetFns = new Set();
|
||||
|
||||
// when creating a store, we get its initial state, create a reset function and add it in the set
|
||||
const create = (createState) => {
|
||||
const store = actualCreate(createState);
|
||||
export const create = (createState) => {
|
||||
const store = zu.create(createState);
|
||||
const initialState = store.getState();
|
||||
storeResetFns.add(() => store.setState(initialState, true));
|
||||
return store;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import produce from 'immer';
|
||||
import type { MultisigControl } from '@vegaprotocol/smart-contracts';
|
||||
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
||||
@@ -120,9 +120,7 @@ export const useEthTransactionStore = create<EthTransactionStore>(
|
||||
produce((state: EthTransactionStore) => {
|
||||
const transaction = state.transactions.find(
|
||||
(transaction) =>
|
||||
transaction &&
|
||||
transaction.status === EthTxStatus.Pending &&
|
||||
deposit.txHash === transaction.txHash
|
||||
transaction && deposit.txHash === transaction.txHash
|
||||
);
|
||||
if (!transaction) {
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import produce from 'immer';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
import type { WithdrawalBusEventFieldsFragment } from '@vegaprotocol/wallet';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import type { Web3ReactHooks } from '@web3-react/core';
|
||||
import type { Connector } from '@web3-react/types';
|
||||
|
||||
|
||||
@@ -186,13 +186,14 @@ describe('WithdrawFormContainer', () => {
|
||||
__typename: 'AccountBalance',
|
||||
},
|
||||
];
|
||||
render(
|
||||
const { container } = render(
|
||||
<MockedProvider>
|
||||
<WithdrawFormContainer {...props} />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('select-asset')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('option')).toHaveLength(3);
|
||||
const options = container.querySelectorAll('select[name="asset"] option');
|
||||
expect(options).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should display no data message', () => {
|
||||
@@ -209,13 +210,14 @@ describe('WithdrawFormContainer', () => {
|
||||
|
||||
it('should filter out zero balance account assets', () => {
|
||||
mockData = [{ ...account1 }, { ...account2, balance: '0' }];
|
||||
render(
|
||||
const { container } = render(
|
||||
<MockedProvider>
|
||||
<WithdrawFormContainer {...props} />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(screen.getByTestId('select-asset')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('option')).toHaveLength(2);
|
||||
const options = container.querySelectorAll('select[name="asset"] option');
|
||||
expect(options).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('when no accounts have a balance should should display no data message', () => {
|
||||
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
Select,
|
||||
RichSelect,
|
||||
Option,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
import type { ControllerRenderProps } from 'react-hook-form';
|
||||
import { useForm, Controller, useWatch } from 'react-hook-form';
|
||||
import type { WithdrawalArgs } from './use-create-withdraw';
|
||||
import { WithdrawLimits } from './withdraw-limits';
|
||||
@@ -85,6 +87,42 @@ export const WithdrawForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const renderAssetsSelector = ({
|
||||
field,
|
||||
}: {
|
||||
field: ControllerRenderProps<FormFields, 'asset'>;
|
||||
}) => {
|
||||
console.log('assets', assets.filter(isAssetTypeERC20));
|
||||
console.log('selected asset', selectedAsset);
|
||||
|
||||
return (
|
||||
<RichSelect
|
||||
data-testid="select-asset"
|
||||
id="asset"
|
||||
name="asset"
|
||||
required
|
||||
onValueChange={(value) => {
|
||||
onSelectAsset(value);
|
||||
field.onChange(value);
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={selectedAsset?.id}
|
||||
hasError={Boolean(errors.asset?.message)}
|
||||
>
|
||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
||||
<Option key={a.id} value={a.id}>
|
||||
<div className="flex flex-col items-start">
|
||||
<span>{a.name}</span>
|
||||
<span className="text-[10px] font-mono">
|
||||
<span className="text-gray-500">{a.id} -</span> {a.symbol}
|
||||
</span>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</RichSelect>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 text-sm">
|
||||
@@ -108,27 +146,7 @@ export const WithdrawForm = ({
|
||||
required: (value) => !!selectedAsset || required(value),
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
onSelectAsset(e.target.value);
|
||||
field.onChange(e.target.value);
|
||||
}}
|
||||
value={selectedAsset?.id || ''}
|
||||
id="asset"
|
||||
name="asset"
|
||||
required
|
||||
data-testid="select-asset"
|
||||
>
|
||||
<option value="">{t('Please select')}</option>
|
||||
{assets.filter(isAssetTypeERC20).map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
render={renderAssetsSelector}
|
||||
/>
|
||||
{errors.asset?.message && (
|
||||
<InputError intent="danger">{errors.asset.message}</InputError>
|
||||
|
||||
@@ -50,9 +50,9 @@ describe('WithdrawManager', () => {
|
||||
);
|
||||
|
||||
it('calls submit if valid form submission', async () => {
|
||||
render(generateJsx(props));
|
||||
const { container } = render(generateJsx(props));
|
||||
await act(async () => {
|
||||
await submitValid();
|
||||
await submitValid(container);
|
||||
});
|
||||
expect(props.submit).toHaveBeenCalledWith({
|
||||
amount: '1000',
|
||||
@@ -98,11 +98,9 @@ describe('WithdrawManager', () => {
|
||||
expect(props.submit).not.toBeCalled();
|
||||
});
|
||||
|
||||
const submitValid = async () => {
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('Asset'),
|
||||
props.assets[0].id
|
||||
);
|
||||
const submitValid = async (container: HTMLElement) => {
|
||||
const select = container.querySelector('select[name="asset"]') as Element;
|
||||
await userEvent.selectOptions(select, props.assets[0].id);
|
||||
fireEvent.change(screen.getByLabelText('To (Ethereum address)'), {
|
||||
target: { value: ethereumAddress },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface WithdrawStore {
|
||||
asset: Asset | undefined;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
+12
-12
@@ -22,17 +22,17 @@
|
||||
"@blueprintjs/icons": "^3.32.0",
|
||||
"@emotion/react": "11.10.0",
|
||||
"@emotion/styled": "11.10.0",
|
||||
"@radix-ui/react-accordion": "^0.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^0.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||
"@radix-ui/react-accordion": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.1",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@radix-ui/react-popover": "^1.0.0",
|
||||
"@radix-ui/react-radio-group": "^0.1.5",
|
||||
"@radix-ui/react-select": "^0.1.1",
|
||||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-tabs": "^0.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.2.0",
|
||||
"@radix-ui/react-slider": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.3",
|
||||
"@sentry/nextjs": "^6.19.3",
|
||||
"@sentry/react": "^6.19.2",
|
||||
"@sentry/tracing": "^6.19.2",
|
||||
@@ -85,7 +85,7 @@
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^2.1.4",
|
||||
"zod": "^3.17.3",
|
||||
"zustand": "^4.0.0-rc.1"
|
||||
"zustand": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/react-testing": "^4.0.0",
|
||||
@@ -183,7 +183,7 @@
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "1.54.0",
|
||||
"storybook-addon-themes": "^6.1.0",
|
||||
"tailwindcss": "3.1.8",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"ts-jest": "27.1.4",
|
||||
"ts-node": "10.9.1",
|
||||
"tslib": "^2.0.0",
|
||||
|
||||
Reference in New Issue
Block a user