Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85a70cb172 | ||
|
|
3a7e9e72af | ||
|
|
5d613396e1 | ||
|
|
da551f9d3c | ||
|
|
47e4861fdb | ||
|
|
1a85a89440 | ||
|
|
7ab412a8f7 | ||
|
|
5a14174a81 | ||
|
|
5e00b93783 | ||
|
|
31d9b023e6 | ||
|
|
103e503a47 | ||
|
|
1952cb0e78 | ||
|
|
69340f4ddf | ||
|
|
c52bf2200e | ||
|
|
8dccee69f2 | ||
|
|
c7a6fdd879 | ||
|
|
8bcdaf4cda |
@@ -50,6 +50,20 @@ jobs:
|
||||
projects=[${projects// /,}]
|
||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||
|
||||
# Rename required because some of the files contains the colon character (in the dates)
|
||||
- name: Rename files to allow archive
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
while read -r file; do
|
||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: logs-${{ matrix.project }}
|
||||
path: /home/runner/.vegacapsule/testnet/logs
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
|
||||
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.sandbox.vega.xyz/websocket
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
|
||||
|
||||
@@ -2,6 +2,7 @@ query ExplorerMarket($id: ID!) {
|
||||
market(id: $id) {
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
|
||||
+2
-1
@@ -8,7 +8,7 @@ export type ExplorerMarketQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
|
||||
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
|
||||
|
||||
|
||||
export const ExplorerMarketDocument = gql`
|
||||
@@ -16,6 +16,7 @@ export const ExplorerMarketDocument = gql`
|
||||
market(id: $id) {
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
|
||||
@@ -55,6 +55,7 @@ describe('Market link component', () => {
|
||||
market: {
|
||||
id: '123',
|
||||
decimalPlaces: 5,
|
||||
positionDecimalPlaces: 2,
|
||||
state: 'irrelevant-test-data',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
|
||||
@@ -19,6 +19,7 @@ fragment ExplorerDeterministicOrderFields on Order {
|
||||
market {
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
|
||||
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
|
||||
|
||||
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
|
||||
orderId: Types.Scalars['ID'];
|
||||
@@ -11,7 +11,7 @@ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
|
||||
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
|
||||
|
||||
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
||||
fragment ExplorerDeterministicOrderFields on Order {
|
||||
@@ -35,6 +35,7 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
|
||||
market {
|
||||
id
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
|
||||
@@ -61,6 +61,7 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
|
||||
__typename: 'Market',
|
||||
id: '789',
|
||||
state: 'STATUS_ACTIVE',
|
||||
positionDecimalPlaces: 2,
|
||||
decimalPlaces: '5',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
@@ -88,6 +89,7 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
|
||||
market: {
|
||||
id: '789',
|
||||
decimalPlaces: 5,
|
||||
positionDecimalPlaces: 2,
|
||||
state: 'irrelevant-test-data',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MarketLink } from '../links';
|
||||
import PriceInMarket from '../price-in-market/price-in-market';
|
||||
import { Time } from '../time';
|
||||
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
|
||||
import SizeInMarket from '../size-in-market/size-in-market';
|
||||
|
||||
export interface DeterministicOrderDetailsProps {
|
||||
id: string;
|
||||
@@ -90,7 +91,7 @@ const DeterministicOrderDetails = ({
|
||||
<div className="mb-12 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
{o.size}
|
||||
<SizeInMarket size={o.size} marketId={o.market.id} />
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const mock = {
|
||||
__typename: 'Market',
|
||||
id: '789',
|
||||
state: 'STATE_ACTIVE',
|
||||
positionDecimalPlaces: 2,
|
||||
decimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
|
||||
@@ -90,6 +90,7 @@ describe('Order TX Summary component', () => {
|
||||
market: {
|
||||
id: '123',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
state: 'irrelevant-test-data',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
|
||||
@@ -37,6 +37,7 @@ const fullMock = {
|
||||
market: {
|
||||
id: '123',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
state: 'irrelevant-test-data',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
|
||||
@@ -66,7 +66,9 @@ export const Search = () => {
|
||||
className="text-white"
|
||||
hasError={Boolean(error?.message)}
|
||||
type="text"
|
||||
placeholder={t('Enter block number, party id or transaction hash')}
|
||||
placeholder={t(
|
||||
'Enter block number, public key or transaction hash'
|
||||
)}
|
||||
/>
|
||||
{error?.message && (
|
||||
<div className="bg-white border border-t-0 border-accent absolute top-[100%] flex-1 w-full pb-2 px-2 rounded-b text-black">
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import SizeInMarket from './size-in-market';
|
||||
import type { DecimalSource } from './size-in-market';
|
||||
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
|
||||
|
||||
function renderComponent(
|
||||
price: string,
|
||||
marketId: string,
|
||||
mocks: MockedResponse[],
|
||||
decimalSource: DecimalSource = 'MARKET'
|
||||
) {
|
||||
return (
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<MemoryRouter>
|
||||
<SizeInMarket
|
||||
marketId={marketId}
|
||||
size={price}
|
||||
decimalSource={decimalSource}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const fullMock = {
|
||||
request: {
|
||||
query: ExplorerMarketDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
id: '123',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
state: 'irrelevant-test-data',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'test dai',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'dai',
|
||||
settlementAsset: {
|
||||
decimals: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Size in Market component', () => {
|
||||
it('Renders the raw size when there is no market data', () => {
|
||||
const res = render(renderComponent('100', '123', []));
|
||||
expect(res.getByText('100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the formatted size when market data is fetched', async () => {
|
||||
const res = render(renderComponent('100', '123', [fullMock]));
|
||||
expect(await res.findByText('1.00')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
|
||||
|
||||
export type DecimalSource = 'MARKET';
|
||||
|
||||
export type PriceInMarketProps = {
|
||||
marketId: string;
|
||||
size: string | number;
|
||||
decimalSource?: DecimalSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a market ID and an order size it will fetch the market
|
||||
* order size, and format the size accordingly
|
||||
*/
|
||||
const SizeInMarket = ({
|
||||
marketId,
|
||||
size,
|
||||
decimalSource = 'MARKET',
|
||||
}: PriceInMarketProps) => {
|
||||
const { data } = useExplorerMarketQuery({
|
||||
variables: { id: marketId },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
let label = size;
|
||||
|
||||
if (data) {
|
||||
if (decimalSource === 'MARKET' && data.market?.positionDecimalPlaces) {
|
||||
label = addDecimalsFormatNumber(size, data.market.positionDecimalPlaces);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<label>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default SizeInMarket;
|
||||
@@ -20,6 +20,7 @@ import { TxDetailsLiquidityCancellation } from './tx-liquidity-cancel';
|
||||
import { TxDetailsDataSubmission } from './tx-data-submission';
|
||||
import { TxProposalVote } from './tx-proposal-vote';
|
||||
import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
|
||||
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -67,6 +68,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
|
||||
// These come from https://github.com/vegaprotocol/vega/blob/develop/core/txn/command.go#L72-L98
|
||||
switch (txData.type) {
|
||||
case 'Issue Signatures':
|
||||
return TxDetailsIssueSignatures;
|
||||
case 'Submit Order':
|
||||
return TxDetailsOrder;
|
||||
case 'Submit Oracle Data':
|
||||
|
||||
@@ -11,8 +11,7 @@ interface TxDetailsGenericProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is not yet a custom component for a transaction, just display
|
||||
* the basic details. This allows someone to view the decoded transaction.
|
||||
* A node is
|
||||
*/
|
||||
export const TxDetailsGeneric = ({
|
||||
txData,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
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 { TableRow, TableCell, TableWithTbody } from '../../table';
|
||||
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import {
|
||||
EthExplorerLink,
|
||||
EthExplorerLinkTypes,
|
||||
} from '../../links/eth-explorer-link/eth-explorer-link';
|
||||
import { NodeLink } from '../../links';
|
||||
|
||||
type Command = components['schemas']['v1IssueSignatures'];
|
||||
|
||||
const kind: Record<components['schemas']['v1NodeSignatureKind'], string> = {
|
||||
NODE_SIGNATURE_KIND_UNSPECIFIED: 'Unspecified',
|
||||
NODE_SIGNATURE_KIND_ASSET_NEW: 'New asset',
|
||||
NODE_SIGNATURE_KIND_ASSET_WITHDRAWAL: 'Asset withdrawal',
|
||||
NODE_SIGNATURE_KIND_ASSET_UPDATE: ' Asset update',
|
||||
NODE_SIGNATURE_KIND_ERC20_MULTISIG_SIGNER_ADDED: 'Multisig signer added',
|
||||
NODE_SIGNATURE_KIND_ERC20_MULTISIG_SIGNER_REMOVED: 'Multisig signer removed',
|
||||
};
|
||||
|
||||
interface TxDetailsGenericProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is not yet a custom component for a transaction, just display
|
||||
* the basic details. This allows someone to view the decoded transaction.
|
||||
*/
|
||||
export const TxDetailsIssueSignatures = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsGenericProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const cmd: Command = txData.command;
|
||||
const k = cmd.kind ? kind[cmd.kind] : null;
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
{k ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Kind')}</TableCell>
|
||||
<TableCell>{k}</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.submitter ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('ETH key')}</TableCell>
|
||||
<TableCell>
|
||||
<EthExplorerLink
|
||||
id={cmd.submitter}
|
||||
type={EthExplorerLinkTypes.address}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.validatorNodeId ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Validator')}</TableCell>
|
||||
<TableCell>
|
||||
<NodeLink id={cmd.validatorNodeId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
export const JumpToParty = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useDocumentTitle(['Parties']);
|
||||
useDocumentTitle(['Public keys']);
|
||||
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -27,8 +27,8 @@ export const JumpToParty = () => {
|
||||
};
|
||||
return (
|
||||
<JumpTo
|
||||
label={t('Go to party')}
|
||||
placeholder={t('Party id')}
|
||||
label={t('Go to public key')}
|
||||
placeholder={t('Public key')}
|
||||
inputId="party-input"
|
||||
inputType="text"
|
||||
inputName="partyId"
|
||||
@@ -40,7 +40,7 @@ export const JumpToParty = () => {
|
||||
const Parties = () => {
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="parties-header">{t('Parties')}</RouteTitle>
|
||||
<RouteTitle data-testid="parties-header">{t('Public keys')}</RouteTitle>
|
||||
<JumpToParty />
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ type PartyIdErrorProps = {
|
||||
const PartyIdError = ({ id, error }: PartyIdErrorProps) => {
|
||||
const end = isValidPartyId(id)
|
||||
? t('No accounts or transactions found for: ')
|
||||
: 'Invalid party id: ';
|
||||
: 'Invalid public key: ';
|
||||
return (
|
||||
<section>
|
||||
<p>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { PartyAccounts } from './components/party-accounts';
|
||||
const Party = () => {
|
||||
const { party } = useParams<{ party: string }>();
|
||||
|
||||
useDocumentTitle(['Parties', party || '-']);
|
||||
useDocumentTitle(['Public keys', party || '-']);
|
||||
const partyId = toNonHex(party ? party : '');
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
||||
@@ -44,7 +44,7 @@ const Party = () => {
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<p>No party found for key {party}</p>
|
||||
<p>No data found for public key {party}</p>
|
||||
</Panel>
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ const Party = () => {
|
||||
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
|
||||
data-testid="parties-header"
|
||||
>
|
||||
{t('Party')}
|
||||
{t('Public key')}
|
||||
</h1>
|
||||
{partyRes.data ? (
|
||||
<>
|
||||
|
||||
@@ -98,13 +98,24 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tranche_id": 53,
|
||||
"tranche_start": "2023-03-06T00:00:00.000Z",
|
||||
"tranche_end": "2023-04-06T00:00:00.000Z",
|
||||
"total_added": "0",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "0",
|
||||
"deposits": [],
|
||||
"withdrawals": [],
|
||||
"users": []
|
||||
},
|
||||
{
|
||||
"tranche_id": 49,
|
||||
"tranche_start": "2022-12-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "71527.9874377224479678513",
|
||||
"locked_amount": "71112.3321837696257405828",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -170,7 +181,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1569.28784467846975",
|
||||
"locked_amount": "1545.241751882377",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -558,7 +569,7 @@
|
||||
"tranche_end": "2023-08-01T00:00:00.000Z",
|
||||
"total_added": "37500",
|
||||
"total_removed": "183.137181525",
|
||||
"locked_amount": "36307.49069597912625",
|
||||
"locked_amount": "35944.80653391651375",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -624,7 +635,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "71462.72714918200044174",
|
||||
"locked_amount": "71047.451128348659886215",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -690,7 +701,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "35715.29892820902776",
|
||||
"locked_amount": "35415.06666032470672",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -883,7 +894,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "3044.440480720446",
|
||||
"locked_amount": "3020.4602676306445",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -1094,7 +1105,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "97499.58",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "13320.4799021065353983616",
|
||||
"locked_amount": "12913.1328103802082906612",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "97499.58",
|
||||
@@ -1127,7 +1138,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "135173.4239508",
|
||||
"total_removed": "98230.390980249184455396",
|
||||
"locked_amount": "18206.796341262034809441102696",
|
||||
"locked_amount": "17650.023192413700622871272848",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "135173.4239508",
|
||||
@@ -1173,7 +1184,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "32499.86",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "5603.6958624323669808404",
|
||||
"locked_amount": "5432.3319754511083747504",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "32499.86",
|
||||
@@ -1206,7 +1217,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "10833.29",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1823.94862624421256173",
|
||||
"locked_amount": "1768.171343907611431555",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10833.29",
|
||||
@@ -1239,7 +1250,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "22749.93",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "6818.3273364692396522472",
|
||||
"locked_amount": "6609.8193974640044998578",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "6500",
|
||||
@@ -1378,7 +1389,7 @@
|
||||
"tranche_end": "2023-05-01T00:00:00.000Z",
|
||||
"total_added": "22500",
|
||||
"total_removed": "3995.28612255",
|
||||
"locked_amount": "10348.030329189685875",
|
||||
"locked_amount": "10130.41983195211725",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -1576,7 +1587,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "928642.9598472029154",
|
||||
"locked_amount": "612507.96463901900484135",
|
||||
"locked_amount": "603203.9854527480689673466",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -2876,7 +2887,7 @@
|
||||
"tranche_start": "2021-09-05T00:00:00.000Z",
|
||||
"tranche_end": "2022-09-30T00:00:00.000Z",
|
||||
"total_added": "60916.66666633337",
|
||||
"total_removed": "34173.43587066019652061",
|
||||
"total_removed": "39088.634764730198755268",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
{
|
||||
@@ -2976,6 +2987,16 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "3395.631843574",
|
||||
"user": "0xa0fF757077B5D796259582b2b9Db99c906277007",
|
||||
"tx": "0x335be76fc5ff37d4272a624f47822e6163d395e0d8a3a9cfcbcc5f7207239361"
|
||||
},
|
||||
{
|
||||
"amount": "1519.567050496002234658",
|
||||
"user": "0x6E14D4e15E245d6ECfe3181B08c2FF30d2c1Da19",
|
||||
"tx": "0x3d0b5ec97be95410571cbc03dd4edd1406243cafbafc40d7c4740f88287efcd3"
|
||||
},
|
||||
{
|
||||
"amount": "1366.363766241335342692",
|
||||
"user": "0xef633C319801eB899354030DFF2BBD0024C0b39c",
|
||||
@@ -3606,6 +3627,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "1519.567050496002234658",
|
||||
"user": "0x6E14D4e15E245d6ECfe3181B08c2FF30d2c1Da19",
|
||||
"tranche_id": 13,
|
||||
"tx": "0x3d0b5ec97be95410571cbc03dd4edd1406243cafbafc40d7c4740f88287efcd3"
|
||||
},
|
||||
{
|
||||
"amount": "747.099616170667765342",
|
||||
"user": "0x6E14D4e15E245d6ECfe3181B08c2FF30d2c1Da19",
|
||||
@@ -3614,8 +3641,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "2266.66666666667",
|
||||
"withdrawn_tokens": "747.099616170667765342",
|
||||
"remaining_tokens": "1519.567050496002234658"
|
||||
"withdrawn_tokens": "2266.66666666667",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xb14025f7eB7717cFF43e7a33b66d86eaBd6bC7d7",
|
||||
@@ -3658,6 +3685,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "3395.631843574",
|
||||
"user": "0xa0fF757077B5D796259582b2b9Db99c906277007",
|
||||
"tranche_id": 13,
|
||||
"tx": "0x335be76fc5ff37d4272a624f47822e6163d395e0d8a3a9cfcbcc5f7207239361"
|
||||
},
|
||||
{
|
||||
"amount": "4.368156426",
|
||||
"user": "0xa0fF757077B5D796259582b2b9Db99c906277007",
|
||||
@@ -3666,8 +3699,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "3400",
|
||||
"withdrawn_tokens": "4.368156426",
|
||||
"remaining_tokens": "3395.631843574"
|
||||
"withdrawn_tokens": "3400",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0x2D69BAB9738b05048be16DE3E5E0A945b8EeEf3a",
|
||||
@@ -33536,8 +33569,8 @@
|
||||
"tranche_start": "2022-03-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "589730.8667090699299",
|
||||
"locked_amount": "965712.81375596155222172624",
|
||||
"total_removed": "592998.0503546212334",
|
||||
"locked_amount": "951415.83704816660962319369",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -33716,6 +33749,11 @@
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tx": "0x8a19d30e4686bca650aa1c4e84a82d8a16d607e07828706cb758a6e17cab0190"
|
||||
},
|
||||
{
|
||||
"amount": "3267.1836455513035",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tx": "0x96af4603ea4ac4ce4585d25325fd739e2380e315175e181c8ac5aec899e5861c"
|
||||
},
|
||||
{
|
||||
"amount": "2536.282963529438",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
@@ -34071,6 +34109,12 @@
|
||||
"tranche_id": 1,
|
||||
"tx": "0x8a19d30e4686bca650aa1c4e84a82d8a16d607e07828706cb758a6e17cab0190"
|
||||
},
|
||||
{
|
||||
"amount": "3267.1836455513035",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tranche_id": 1,
|
||||
"tx": "0x96af4603ea4ac4ce4585d25325fd739e2380e315175e181c8ac5aec899e5861c"
|
||||
},
|
||||
{
|
||||
"amount": "2536.282963529438",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
@@ -34337,8 +34381,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "187637.95",
|
||||
"withdrawn_tokens": "136494.1152043067095",
|
||||
"remaining_tokens": "51143.8347956932905"
|
||||
"withdrawn_tokens": "139761.298849858013",
|
||||
"remaining_tokens": "47876.651150141987"
|
||||
},
|
||||
{
|
||||
"address": "0x1A71e3ED1996CAbB91bB043f880CE963D601707e",
|
||||
@@ -34851,8 +34895,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "557133.30560592463281452",
|
||||
"locked_amount": "8724043.2185305122313058695420656514828332",
|
||||
"total_removed": "560497.18500562348693952",
|
||||
"locked_amount": "8673347.0570783123228140520127045570383887",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -35416,6 +35460,21 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xa344428c2b1bf9b4685959441983da46bcd434ac0ee5ada325876c8733eba603"
|
||||
},
|
||||
{
|
||||
"amount": "2667.021494",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tx": "0x94fe358ee7c973dd1d28ca4c56250dfaccbcb718673872c8e9cf8651a22a1108"
|
||||
},
|
||||
{
|
||||
"amount": "517.348758134784125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xb6a33b8b8855789fbc2d0f9545ba5c2258e52f143d90bc75bbe2411bd2f9b4a5"
|
||||
},
|
||||
{
|
||||
"amount": "179.50914756407",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tx": "0x86280b4d0cccc9e251d0f18fce5af974d0e1fb31a1aaf219725a27efb0d607f4"
|
||||
},
|
||||
{
|
||||
"amount": "858.360074993579125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -37035,6 +37094,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0xa344428c2b1bf9b4685959441983da46bcd434ac0ee5ada325876c8733eba603"
|
||||
},
|
||||
{
|
||||
"amount": "517.348758134784125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xb6a33b8b8855789fbc2d0f9545ba5c2258e52f143d90bc75bbe2411bd2f9b4a5"
|
||||
},
|
||||
{
|
||||
"amount": "858.360074993579125",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -38123,8 +38188,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "259998.8875",
|
||||
"withdrawn_tokens": "116966.74361795744475",
|
||||
"remaining_tokens": "143032.14388204255525"
|
||||
"withdrawn_tokens": "117484.092376092228875",
|
||||
"remaining_tokens": "142514.795123907771125"
|
||||
},
|
||||
{
|
||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||
@@ -38778,6 +38843,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x606ddaa3882cccb0062bc2827cfcfceb64cef8a6c4df41d06747ae651e5dd52e"
|
||||
},
|
||||
{
|
||||
"amount": "2667.021494",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x94fe358ee7c973dd1d28ca4c56250dfaccbcb718673872c8e9cf8651a22a1108"
|
||||
},
|
||||
{
|
||||
"amount": "1099.300488",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
@@ -38960,8 +39031,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200000",
|
||||
"withdrawn_tokens": "87428.726512",
|
||||
"remaining_tokens": "112571.273488"
|
||||
"withdrawn_tokens": "90095.748006",
|
||||
"remaining_tokens": "109904.251994"
|
||||
},
|
||||
{
|
||||
"address": "0x1b956E6c00E238194B331eddEFF72Cb5f28A8d01",
|
||||
@@ -39427,6 +39498,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0xeb5f2401c9402d58fa4a7549ce2045a48a2c3b90ec6e812ca2f0059f1275e213"
|
||||
},
|
||||
{
|
||||
"amount": "179.50914756407",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x86280b4d0cccc9e251d0f18fce5af974d0e1fb31a1aaf219725a27efb0d607f4"
|
||||
},
|
||||
{
|
||||
"amount": "139.3487773806265",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
@@ -39675,8 +39752,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "12362.05",
|
||||
"withdrawn_tokens": "5423.9024394185135",
|
||||
"remaining_tokens": "6938.1475605814865"
|
||||
"withdrawn_tokens": "5603.4115869825835",
|
||||
"remaining_tokens": "6758.6384130174165"
|
||||
},
|
||||
{
|
||||
"address": "0xb091D456d0dFCB94dcba6f355379056C5bb995fC",
|
||||
@@ -40307,8 +40384,8 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "3920990.096830703963164282",
|
||||
"locked_amount": "2332535.769515174096512550505057687",
|
||||
"total_removed": "3921707.576409189114087282",
|
||||
"locked_amount": "2285733.39696055204772238928937325",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129284.449",
|
||||
@@ -40567,6 +40644,11 @@
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xc73c47dcfac4093bb6325fe92986dd010d5a773af4bb97e188ad57359a309a52"
|
||||
},
|
||||
{
|
||||
"amount": "717.479578485150923",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x0c292f2355b5cdc3b389ed35e6e138e593b1db9d91fcd58f132835343724bf90"
|
||||
},
|
||||
{
|
||||
"amount": "8950.14985089483210984",
|
||||
"user": "0x66827bCD635f2bB1779d68c46aEB16541bCA6ba8",
|
||||
@@ -43404,6 +43486,12 @@
|
||||
"tranche_id": 3,
|
||||
"tx": "0xc73c47dcfac4093bb6325fe92986dd010d5a773af4bb97e188ad57359a309a52"
|
||||
},
|
||||
{
|
||||
"amount": "717.479578485150923",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x0c292f2355b5cdc3b389ed35e6e138e593b1db9d91fcd58f132835343724bf90"
|
||||
},
|
||||
{
|
||||
"amount": "1192.05386354121365675",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -45794,8 +45882,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "359123.469575",
|
||||
"withdrawn_tokens": "301591.90406015526256375",
|
||||
"remaining_tokens": "57531.56551484473743625"
|
||||
"withdrawn_tokens": "302309.38363864041348675",
|
||||
"remaining_tokens": "56814.08593635958651325"
|
||||
},
|
||||
{
|
||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||
@@ -47133,7 +47221,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "2730068.739915456784546642",
|
||||
"locked_amount": "604695.586054148570133080743399363",
|
||||
"locked_amount": "586203.563337309134341622543222115",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "552496.6455",
|
||||
@@ -49115,8 +49203,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "32361.4666889012685",
|
||||
"locked_amount": "153022.71558941830041836828209032",
|
||||
"total_removed": "32520.5457984892685",
|
||||
"locked_amount": "150757.26292676673422831001319128",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
@@ -55775,6 +55863,21 @@
|
||||
"user": "0x0e199b123f71f964d6567869B6B849C1f255A855",
|
||||
"tx": "0x6df8666e49f7d491c740d6a36df031aac232a6cf37fa197aa1f6b2bb481be99c"
|
||||
},
|
||||
{
|
||||
"amount": "99.47735921",
|
||||
"user": "0x2E32F49389CF3039ab6365ca59329002922Cc01D",
|
||||
"tx": "0x7de3c1703d23970f04ce1bebe2cece8a2fe3fbe6b876502da1812c312151b954"
|
||||
},
|
||||
{
|
||||
"amount": "15.80745814",
|
||||
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
|
||||
"tx": "0xc0f953d7ecc45bfc1d7b69ecd491a63e8b00b0d732ff006bee008e917a04179f"
|
||||
},
|
||||
{
|
||||
"amount": "43.794292238",
|
||||
"user": "0x916E6Ce65e828e725Affdeda2C0a083071188109",
|
||||
"tx": "0xdad3bca9bdda18abd6bbed7c1d618f60316bafc507591966b7a86bed3dc36b39"
|
||||
},
|
||||
{
|
||||
"amount": "78.261187214",
|
||||
"user": "0x479F059c46c6873C6B970A92911084b3eA036d56",
|
||||
@@ -68030,6 +68133,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "99.47735921",
|
||||
"user": "0x2E32F49389CF3039ab6365ca59329002922Cc01D",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x7de3c1703d23970f04ce1bebe2cece8a2fe3fbe6b876502da1812c312151b954"
|
||||
},
|
||||
{
|
||||
"amount": "35.842421358",
|
||||
"user": "0x2E32F49389CF3039ab6365ca59329002922Cc01D",
|
||||
@@ -68038,8 +68147,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "35.842421358",
|
||||
"remaining_tokens": "164.157578642"
|
||||
"withdrawn_tokens": "135.319780568",
|
||||
"remaining_tokens": "64.680219432"
|
||||
},
|
||||
{
|
||||
"address": "0x2F2588aCd44253312b4A94bF6753bE67514A5Cc6",
|
||||
@@ -69114,6 +69223,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "15.80745814",
|
||||
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xc0f953d7ecc45bfc1d7b69ecd491a63e8b00b0d732ff006bee008e917a04179f"
|
||||
},
|
||||
{
|
||||
"amount": "32.761796044",
|
||||
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
|
||||
@@ -69188,8 +69303,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "254.83529934",
|
||||
"remaining_tokens": "145.16470066"
|
||||
"withdrawn_tokens": "270.64275748",
|
||||
"remaining_tokens": "129.35724252"
|
||||
},
|
||||
{
|
||||
"address": "0x5c90765F50629570738fEe7b7FA82ae118f81Ed1",
|
||||
@@ -75901,6 +76016,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "43.794292238",
|
||||
"user": "0x916E6Ce65e828e725Affdeda2C0a083071188109",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xdad3bca9bdda18abd6bbed7c1d618f60316bafc507591966b7a86bed3dc36b39"
|
||||
},
|
||||
{
|
||||
"amount": "11.442313546",
|
||||
"user": "0x916E6Ce65e828e725Affdeda2C0a083071188109",
|
||||
@@ -75915,8 +76036,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200",
|
||||
"withdrawn_tokens": "92.326249364",
|
||||
"remaining_tokens": "107.673750636"
|
||||
"withdrawn_tokens": "136.120541602",
|
||||
"remaining_tokens": "63.879458398"
|
||||
},
|
||||
{
|
||||
"address": "0x93D9D57409e0a1fD340b270Cd41368cc66392249",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export const ProposalFormTransactionDialog = ({
|
||||
finalizedProposal,
|
||||
TransactionDialog,
|
||||
}: ProposalFormTransactionDialogProps) => {
|
||||
// Render a custom complete UI if the proposal was rejected other wise
|
||||
// Render a custom complete UI if the proposal was rejected otherwise
|
||||
// pass undefined so that the default vega transaction dialog UI gets used
|
||||
const completeContent = finalizedProposal?.rejectionReason ? (
|
||||
<p>{finalizedProposal.rejectionReason}</p>
|
||||
|
||||
@@ -90,22 +90,15 @@ describe('Raw proposal form', () => {
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
busEvents: [
|
||||
{
|
||||
__typename: 'BusEvent',
|
||||
type: Schema.BusEventType.Proposal,
|
||||
event: {
|
||||
__typename: 'Proposal',
|
||||
id: '2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50',
|
||||
reference: 'proposal-reference',
|
||||
state: Schema.ProposalState.STATE_OPEN,
|
||||
rejectionReason:
|
||||
Schema.ProposalRejectionReason
|
||||
.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
|
||||
errorDetails: 'error-details',
|
||||
},
|
||||
},
|
||||
],
|
||||
proposals: {
|
||||
__typename: 'Proposal',
|
||||
id: '2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50',
|
||||
reference: 'proposal-reference',
|
||||
state: Schema.ProposalState.STATE_OPEN,
|
||||
rejectionReason:
|
||||
Schema.ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
|
||||
errorDetails: 'error-details',
|
||||
},
|
||||
},
|
||||
},
|
||||
delay: 300,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useWithdrawalDialog } from '@vegaprotocol/withdraws';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountManager } from '@vegaprotocol/accounts';
|
||||
import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts';
|
||||
import { useDepositDialog } from '@vegaprotocol/deposits';
|
||||
|
||||
export const AccountsContainer = () => {
|
||||
@@ -13,6 +13,7 @@ export const AccountsContainer = () => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
|
||||
const openDepositDialog = useDepositDialog((store) => store.open);
|
||||
const openTransferDialog = useTransferDialog((store) => store.open);
|
||||
|
||||
const onClickAsset = useCallback(
|
||||
(assetId?: string) => {
|
||||
@@ -41,7 +42,10 @@ export const AccountsContainer = () => {
|
||||
/>
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
<div className="flex justify-end p-2 px-[11px]">
|
||||
<div className="flex gap-2 justify-end p-2 px-[11px]">
|
||||
<Button size="sm" onClick={() => openTransferDialog()}>
|
||||
{t('Transfer')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => openDepositDialog()}>
|
||||
{t('Deposit')}
|
||||
</Button>
|
||||
|
||||
@@ -13,11 +13,13 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
Icon,
|
||||
Drawer,
|
||||
DropdownMenuSeparator,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { PubKey } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { WalletIcon } from '../icons/wallet';
|
||||
import { useTransferDialog } from '@vegaprotocol/accounts';
|
||||
|
||||
const MobileWalletButton = ({
|
||||
isConnected,
|
||||
@@ -30,6 +32,7 @@ const MobileWalletButton = ({
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
const openTransferDialog = useTransferDialog((store) => store.open);
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const isYellow = VEGA_ENV === Networks.TESTNET;
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
@@ -115,7 +118,16 @@ const MobileWalletButton = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="m-4">
|
||||
<div className="flex flex-col gap-2 m-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDrawerOpen(false);
|
||||
openTransferDialog(true);
|
||||
}}
|
||||
fill
|
||||
>
|
||||
{t('Transfer')}
|
||||
</Button>
|
||||
<Button onClick={mobileDisconnect} fill>
|
||||
{t('Disconnect')}
|
||||
</Button>
|
||||
@@ -131,6 +143,7 @@ export const VegaWalletConnectButton = () => {
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
const openTransferDialog = useTransferDialog((store) => store.open);
|
||||
const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet();
|
||||
const isConnected = pubKey !== null;
|
||||
|
||||
@@ -171,6 +184,10 @@ export const VegaWalletConnectButton = () => {
|
||||
<KeypairItem key={pk.publicKey} pk={pk} />
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => openTransferDialog(true)}>
|
||||
{t('Transfer')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem data-testid="disconnect" onClick={disconnect}>
|
||||
{t('Disconnect')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -3,8 +3,12 @@ import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { ETHERSCAN_TX, useEtherscanLink } from '@vegaprotocol/environment';
|
||||
import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { Panel } from '@vegaprotocol/ui-toolkit';
|
||||
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
import {
|
||||
@@ -44,34 +48,35 @@ const EthTransactionDetails = ({ tx }: { tx: EthStoredTxState }) => {
|
||||
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}{' '}
|
||||
{formatNumber(toBigNum(tx.args[1], asset.decimals), asset.decimals)}{' '}
|
||||
{asset.symbol}
|
||||
</span>
|
||||
</div>
|
||||
<strong>
|
||||
{label}{' '}
|
||||
{formatNumber(toBigNum(tx.args[1], asset.decimals), asset.decimals)}{' '}
|
||||
{asset.symbol}
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
if (assetInfo || tx.requiresConfirmation) {
|
||||
return (
|
||||
<Panel>
|
||||
{assetInfo}
|
||||
{tx.status === EthTxStatus.Pending && (
|
||||
<>
|
||||
<p className="mt-[2px]">
|
||||
{t('Awaiting confirmations')}{' '}
|
||||
{`(${tx.confirmations}/${tx.requiredConfirmations})`}
|
||||
</p>
|
||||
<ProgressBar
|
||||
value={(tx.confirmations / tx.requiredConfirmations) * 100}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type EthTxToastContentProps = {
|
||||
@@ -80,26 +85,26 @@ type EthTxToastContentProps = {
|
||||
|
||||
const EthTxRequestedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Action required')}</h3>
|
||||
<>
|
||||
<ToastHeading>{t('Action required')}</ToastHeading>
|
||||
<p>
|
||||
{t(
|
||||
'Please go to your wallet application and approve or reject the transaction.'
|
||||
)}
|
||||
</p>
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
|
||||
<>
|
||||
<ToastHeading>{t('Awaiting confirmation')}</ToastHeading>
|
||||
<p>{t('Please wait for your transaction to be confirmed.')}</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -112,11 +117,11 @@ const EthTxErrorToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
errorMessage = tx.error.message;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Error occurred')}</h3>
|
||||
<p>{errorMessage}</p>
|
||||
<>
|
||||
<ToastHeading>{t('Error occurred')}</ToastHeading>
|
||||
<p className="first-letter:uppercase">{errorMessage}</p>
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,42 +141,63 @@ const EtherscanLink = ({ tx }: EthTxToastContentProps) => {
|
||||
|
||||
const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Transaction confirmed')}</h3>
|
||||
<>
|
||||
<ToastHeading>{t('Transaction confirmed')}</ToastHeading>
|
||||
<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">
|
||||
<>
|
||||
<ToastHeading>
|
||||
{t('Processing')} {isDeposit && t('deposit')}
|
||||
</h3>
|
||||
</ToastHeading>
|
||||
<p>
|
||||
{t('Your transaction has been completed.')}{' '}
|
||||
{isDeposit && t('Waiting for deposit confirmation.')}
|
||||
</p>
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const isFinal = (tx: EthStoredTxState) =>
|
||||
[EthTxStatus.Confirmed, EthTxStatus.Error].includes(tx.status);
|
||||
|
||||
export const useEthereumTransactionToasts = () => {
|
||||
const ethTransactions = useEthTransactionStore((state) =>
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
);
|
||||
const dismissEthTransaction = useEthTransactionStore(
|
||||
(state) => state.dismiss
|
||||
const [setToast, removeToast] = useToasts((store) => [
|
||||
store.setToast,
|
||||
store.remove,
|
||||
]);
|
||||
|
||||
const [dismissTx, deleteTx] = useEthTransactionStore((state) => [
|
||||
state.dismiss,
|
||||
state.delete,
|
||||
]);
|
||||
|
||||
const onClose = useCallback(
|
||||
(tx: EthStoredTxState) => () => {
|
||||
const safeToDelete = isFinal(tx);
|
||||
if (safeToDelete) {
|
||||
deleteTx(tx.id);
|
||||
} else {
|
||||
dismissTx(tx.id);
|
||||
}
|
||||
removeToast(`eth-${tx.id}`);
|
||||
},
|
||||
[deleteTx, dismissTx, removeToast]
|
||||
);
|
||||
|
||||
const fromEthTransaction = useCallback(
|
||||
(tx: EthStoredTxState): Toast => {
|
||||
let content: ToastContent = <TransactionContent {...tx} />;
|
||||
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
|
||||
if (tx.status === EthTxStatus.Requested) {
|
||||
content = <EthTxRequestedToastContent tx={tx} />;
|
||||
}
|
||||
@@ -191,15 +217,21 @@ export const useEthereumTransactionToasts = () => {
|
||||
return {
|
||||
id: `eth-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
onClose: () => dismissEthTransaction(tx.id),
|
||||
onClose: onClose(tx),
|
||||
loader: [EthTxStatus.Pending, EthTxStatus.Complete].includes(tx.status),
|
||||
content,
|
||||
closeAfter,
|
||||
};
|
||||
},
|
||||
[dismissEthTransaction]
|
||||
[onClose]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return [...compact(ethTransactions).map(fromEthTransaction)];
|
||||
}, [ethTransactions, fromEthTransaction]);
|
||||
useEthTransactionStore.subscribe(
|
||||
(state) => compact(state.transactions.filter((tx) => tx?.dialogOpen)),
|
||||
(txs) => {
|
||||
txs.forEach((tx) => {
|
||||
setToast(fromEthTransaction(tx));
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { Panel } from '@vegaprotocol/ui-toolkit';
|
||||
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { ApprovalStatus, VerificationStatus } from '@vegaprotocol/withdraws';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import type { EthWithdrawalApprovalState } from '@vegaprotocol/web3';
|
||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||
@@ -30,49 +34,72 @@ const EthWithdrawalApprovalToastContent = ({
|
||||
if (tx.status === ApprovalStatus.Delayed) {
|
||||
title = t('Delayed');
|
||||
}
|
||||
if (tx.status === ApprovalStatus.Ready) {
|
||||
title = t('Approved');
|
||||
}
|
||||
const num = formatNumber(
|
||||
toBigNum(tx.withdrawal.amount, tx.withdrawal.asset.decimals),
|
||||
tx.withdrawal.asset.decimals
|
||||
);
|
||||
const details = (
|
||||
<div className="mt-[5px]">
|
||||
<span className="font-mono text-xs p-1 bg-gray-100 rounded">
|
||||
<Panel>
|
||||
<strong>
|
||||
{t('Withdraw')} {num} {tx.withdrawal.asset.symbol}
|
||||
</span>
|
||||
</div>
|
||||
</strong>
|
||||
</Panel>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{title.length > 0 && <h3 className="font-bold">{title}</h3>}
|
||||
<>
|
||||
{title.length > 0 && (
|
||||
<ToastHeading className="font-bold">{title}</ToastHeading>
|
||||
)}
|
||||
<VerificationStatus state={tx} />
|
||||
{details}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const isFinal = (tx: EthWithdrawalApprovalState) =>
|
||||
[ApprovalStatus.Ready, ApprovalStatus.Error].includes(tx.status);
|
||||
|
||||
export const useEthereumWithdrawApprovalsToasts = () => {
|
||||
const { withdrawApprovals, dismissWithdrawApproval } =
|
||||
useEthWithdrawApprovalsStore((state) => ({
|
||||
withdrawApprovals: state.transactions.filter(
|
||||
(transaction) => transaction?.dialogOpen
|
||||
),
|
||||
dismissWithdrawApproval: state.dismiss,
|
||||
}));
|
||||
const [setToast, remove] = useToasts((state) => [
|
||||
state.setToast,
|
||||
state.remove,
|
||||
]);
|
||||
const [dismissTx, deleteTx] = useEthWithdrawApprovalsStore((state) => [
|
||||
state.dismiss,
|
||||
state.delete,
|
||||
]);
|
||||
|
||||
const fromWithdrawalApproval = useCallback(
|
||||
(tx: EthWithdrawalApprovalState): Toast => ({
|
||||
id: `withdrawal-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
onClose: () => dismissWithdrawApproval(tx.id),
|
||||
onClose: () => {
|
||||
if ([ApprovalStatus.Error, ApprovalStatus.Ready].includes(tx.status)) {
|
||||
deleteTx(tx.id);
|
||||
} else {
|
||||
dismissTx(tx.id);
|
||||
}
|
||||
remove(`withdrawal-${tx.id}`);
|
||||
},
|
||||
loader: tx.status === ApprovalStatus.Pending,
|
||||
content: <EthWithdrawalApprovalToastContent tx={tx} />,
|
||||
closeAfter: isFinal(tx) ? CLOSE_AFTER : undefined,
|
||||
}),
|
||||
[dismissWithdrawApproval]
|
||||
[deleteTx, dismissTx, remove]
|
||||
);
|
||||
|
||||
const toasts = useMemo(() => {
|
||||
return [...compact(withdrawApprovals).map(fromWithdrawalApproval)];
|
||||
}, [fromWithdrawalApproval, withdrawApprovals]);
|
||||
|
||||
return toasts;
|
||||
useEthWithdrawApprovalsStore.subscribe(
|
||||
(state) =>
|
||||
compact(
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
),
|
||||
(txs) => {
|
||||
txs.forEach((tx) => {
|
||||
setToast(fromWithdrawalApproval(tx));
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -260,7 +260,7 @@ describe('VegaTransactionDetails', () => {
|
||||
const { queryByTestId } = render(
|
||||
<VegaTransactionDetails tx={unsupportedTransaction} />
|
||||
);
|
||||
expect(queryByTestId('vega-tx-details')).toBeNull();
|
||||
expect(queryByTestId('toast-panel')).toBeNull();
|
||||
});
|
||||
it.each([
|
||||
{ tx: withdraw, details: 'Withdraw 12.34 $A' },
|
||||
@@ -275,6 +275,6 @@ describe('VegaTransactionDetails', () => {
|
||||
{ tx: batch, details: 'Batch market instruction' },
|
||||
])('display details for transaction', ({ tx, details }) => {
|
||||
const { queryByTestId } = render(<VegaTransactionDetails tx={tx} />);
|
||||
expect(queryByTestId('vega-tx-details')?.textContent).toEqual(details);
|
||||
expect(queryByTestId('toast-panel')?.textContent).toEqual(details);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import first from 'lodash/first';
|
||||
import compact from 'lodash/compact';
|
||||
import type {
|
||||
BatchMarketInstructionSubmissionBody,
|
||||
@@ -10,13 +10,12 @@ import type {
|
||||
VegaStoredTxState,
|
||||
WithdrawalBusEventFieldsFragment,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { isBatchMarketInstructionsTransaction } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
isTransferTransaction,
|
||||
isBatchMarketInstructionsTransaction,
|
||||
ClientErrors,
|
||||
useReconnectVegaWallet,
|
||||
WalletError,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
isOrderAmendmentTransaction,
|
||||
isOrderCancellationTransaction,
|
||||
isOrderSubmissionTransaction,
|
||||
@@ -25,6 +24,10 @@ import {
|
||||
VegaTxStatus,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { Panel } from '@vegaprotocol/ui-toolkit';
|
||||
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
@@ -32,14 +35,15 @@ import {
|
||||
Size,
|
||||
t,
|
||||
toBigNum,
|
||||
truncateByChars,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
||||
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
|
||||
import { useMarketList } from '@vegaprotocol/market-list';
|
||||
import first from 'lodash/first';
|
||||
import type { Side } from '@vegaprotocol/types';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||
|
||||
const intentMap: { [s in VegaTxStatus]: Intent } = {
|
||||
@@ -50,15 +54,6 @@ const intentMap: { [s in VegaTxStatus]: Intent } = {
|
||||
Complete: Intent.Success,
|
||||
};
|
||||
|
||||
const getIntent = (tx: VegaStoredTxState) => {
|
||||
// Transaction can be successful
|
||||
// But the order can be rejected by the network
|
||||
if (tx.order?.rejectionReason) {
|
||||
return Intent.Danger;
|
||||
}
|
||||
return intentMap[tx.status];
|
||||
};
|
||||
|
||||
const isClosePositionTransaction = (tx: VegaStoredTxState) => {
|
||||
if (isBatchMarketInstructionsTransaction(tx.body)) {
|
||||
const amendments =
|
||||
@@ -87,29 +82,17 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
||||
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
||||
const editOrder = isOrderAmendmentTransaction(tx.body);
|
||||
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
|
||||
const transfer = isTransferTransaction(tx.body);
|
||||
return (
|
||||
withdraw ||
|
||||
submitOrder ||
|
||||
cancelOrder ||
|
||||
editOrder ||
|
||||
batchMarketInstructions
|
||||
batchMarketInstructions ||
|
||||
transfer
|
||||
);
|
||||
};
|
||||
|
||||
const Details = ({
|
||||
children,
|
||||
title = '',
|
||||
}: {
|
||||
children: ReactNode;
|
||||
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 dark:bg-neutral-700 dark:text-white">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type SizeAtPriceProps = {
|
||||
side: Side;
|
||||
size: string;
|
||||
@@ -150,8 +133,8 @@ const SubmitOrderDetails = ({
|
||||
const side = order ? order.side : data.side;
|
||||
|
||||
return (
|
||||
<Details>
|
||||
<h4 className="font-bold">
|
||||
<Panel>
|
||||
<h4>
|
||||
{order
|
||||
? t(
|
||||
`Submit order - ${OrderStatusMapping[order.status].toLowerCase()}`
|
||||
@@ -173,10 +156,7 @@ const SubmitOrderDetails = ({
|
||||
price={price}
|
||||
/>
|
||||
</p>
|
||||
{order && order.rejectionReason && (
|
||||
<p className="italic">{getRejectionReason(order)}</p>
|
||||
)}
|
||||
</Details>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -192,9 +172,10 @@ const EditOrderDetails = ({
|
||||
});
|
||||
const { data: markets } = useMarketList();
|
||||
|
||||
const originalOrder = orderById?.orderByID;
|
||||
const originalOrder = order || orderById?.orderByID;
|
||||
const marketId = order?.marketId || orderById?.orderByID.market.id;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.find((m) => m.id === originalOrder.market.id);
|
||||
const market = markets?.find((m) => m.id === marketId);
|
||||
if (!market) return null;
|
||||
|
||||
const original = (
|
||||
@@ -226,8 +207,8 @@ const EditOrderDetails = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Details title={data.orderId}>
|
||||
<h4 className="font-bold">
|
||||
<Panel title={data.orderId}>
|
||||
<h4>
|
||||
{order
|
||||
? t(`Edit order - ${OrderStatusMapping[order.status].toLowerCase()}`)
|
||||
: t('Edit order')}
|
||||
@@ -237,10 +218,7 @@ const EditOrderDetails = ({
|
||||
<s>{original}</s>
|
||||
</p>
|
||||
<p>{edited}</p>
|
||||
{order && order.rejectionReason && (
|
||||
<p className="italic">{getRejectionReason(order)}</p>
|
||||
)}
|
||||
</Details>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -275,8 +253,8 @@ const CancelOrderDetails = ({
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Details title={orderId}>
|
||||
<h4 className="font-bold">
|
||||
<Panel title={orderId}>
|
||||
<h4>
|
||||
{order
|
||||
? t(
|
||||
`Cancel order - ${OrderStatusMapping[order.status].toLowerCase()}`
|
||||
@@ -287,10 +265,7 @@ const CancelOrderDetails = ({
|
||||
<p>
|
||||
<s>{original}</s>
|
||||
</p>
|
||||
{order && order.rejectionReason && (
|
||||
<p className="italic">{getRejectionReason(order)}</p>
|
||||
)}
|
||||
</Details>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -309,9 +284,11 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
asset.decimals
|
||||
);
|
||||
return (
|
||||
<Details>
|
||||
{t('Withdraw')} {num} {asset.symbol}
|
||||
</Details>
|
||||
<Panel>
|
||||
<strong>
|
||||
{t('Withdraw')} {num} {asset.symbol}
|
||||
</strong>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -328,7 +305,7 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
tx.body.orderCancellation.marketId === undefined &&
|
||||
tx.body.orderCancellation.orderId === undefined
|
||||
) {
|
||||
return <Details>{t('Cancel all orders')}</Details>;
|
||||
return <Panel>{t('Cancel all orders')}</Panel>;
|
||||
}
|
||||
|
||||
// CANCEL
|
||||
@@ -351,11 +328,15 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
m.id === (tx.body as OrderCancellationBody).orderCancellation.marketId
|
||||
)?.tradableInstrument.instrument.code;
|
||||
return (
|
||||
<Details>
|
||||
{marketName
|
||||
? `${t('Cancel all orders for')} ${marketName}`
|
||||
: t('Cancel all orders')}
|
||||
</Details>
|
||||
<Panel>
|
||||
{marketName ? (
|
||||
<>
|
||||
{t('Cancel all orders for')} <strong>{marketName}</strong>
|
||||
</>
|
||||
) : (
|
||||
t('Cancel all orders')
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -377,15 +358,36 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
const market = marketId && markets?.find((m) => m.id === marketId);
|
||||
if (market) {
|
||||
return (
|
||||
<Details>
|
||||
{t('Close position for')} {market.tradableInstrument.instrument.code}
|
||||
</Details>
|
||||
<Panel>
|
||||
{t('Close position for')}{' '}
|
||||
<strong>{market.tradableInstrument.instrument.code}</strong>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isBatchMarketInstructionsTransaction(tx.body)) {
|
||||
return <Details>{t('Batch market instruction')}</Details>;
|
||||
return <Panel>{t('Batch market instruction')}</Panel>;
|
||||
}
|
||||
|
||||
if (isTransferTransaction(tx.body)) {
|
||||
const { amount, to, asset } = tx.body.transfer;
|
||||
const transferAsset = assets?.find((a) => a.id === asset);
|
||||
// only render if we have an asset to avoid unformatted amounts showing
|
||||
if (transferAsset) {
|
||||
const value = addDecimalsFormatNumber(amount, transferAsset.decimals);
|
||||
return (
|
||||
<Panel>
|
||||
<h4>{t('Transfer')}</h4>
|
||||
<p>
|
||||
{t('To')} {truncateByChars(to)}
|
||||
</p>
|
||||
<p>
|
||||
{value} {transferAsset.symbol}
|
||||
</p>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -394,22 +396,22 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
type VegaTxToastContentProps = { tx: VegaStoredTxState };
|
||||
|
||||
const VegaTxRequestedToastContent = ({ tx }: VegaTxToastContentProps) => (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Action required')}</h3>
|
||||
<>
|
||||
<ToastHeading>{t('Action required')}</ToastHeading>
|
||||
<p>
|
||||
{t(
|
||||
'Please go to your Vega wallet application and approve or reject the transaction.'
|
||||
)}
|
||||
</p>
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const VegaTxPendingToastContentProps = ({ tx }: VegaTxToastContentProps) => {
|
||||
const explorerLink = useLinks(DApp.Explorer);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
|
||||
<>
|
||||
<ToastHeading>{t('Awaiting confirmation')}</ToastHeading>
|
||||
<p>{t('Please wait for your transaction to be confirmed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
@@ -422,7 +424,7 @@ const VegaTxPendingToastContentProps = ({ tx }: VegaTxToastContentProps) => {
|
||||
</p>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -433,9 +435,10 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
})
|
||||
);
|
||||
const explorerLink = useLinks(DApp.Explorer);
|
||||
|
||||
if (isWithdrawTransaction(tx.body)) {
|
||||
const completeWithdrawalButton = tx.withdrawal && (
|
||||
<div className="mt-[10px]">
|
||||
<p className="mt-1">
|
||||
<Button
|
||||
data-testid="toast-complete-withdrawal"
|
||||
size="xs"
|
||||
@@ -448,11 +451,11 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
>
|
||||
{t('Complete withdrawal')}
|
||||
</Button>
|
||||
</div>
|
||||
</p>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Funds unlocked')}</h3>
|
||||
<>
|
||||
<ToastHeading>{t('Funds unlocked')}</ToastHeading>
|
||||
<p>{t('Your funds have been unlocked for withdrawal')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
@@ -466,7 +469,32 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
{completeWithdrawalButton}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (tx.order && tx.order.rejectionReason) {
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Order rejected')}</ToastHeading>
|
||||
<p>
|
||||
{t(
|
||||
'Your order has been rejected because: %s',
|
||||
getRejectionReason(tx.order) || ''
|
||||
)}
|
||||
</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={explorerLink(EXPLORER_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View in block explorer')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -490,9 +518,19 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isTransferTransaction(tx.body)) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Transfer complete')}</h3>
|
||||
<p>{t('Your transaction has been confirmed ')}</p>
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Confirmed')}</h3>
|
||||
<>
|
||||
<ToastHeading>{t('Confirmed')}</ToastHeading>
|
||||
<p>{t('Your transaction has been confirmed ')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
@@ -505,7 +543,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
</p>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -528,7 +566,10 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
walletNoConnectionCodes.includes(tx.error.code);
|
||||
if (orderRejection) {
|
||||
label = t('Order rejected');
|
||||
errorMessage = orderRejection;
|
||||
errorMessage = t(
|
||||
'Your order has been rejected because: %s',
|
||||
orderRejection
|
||||
);
|
||||
}
|
||||
if (walletError) {
|
||||
label = t('Wallet disconnected');
|
||||
@@ -536,60 +577,87 @@ const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{label}</h3>
|
||||
<p>{errorMessage}</p>
|
||||
<>
|
||||
<ToastHeading>{label}</ToastHeading>
|
||||
<p className="first-letter:uppercase">{errorMessage}</p>
|
||||
{walletError && (
|
||||
<Button size="xs" onClick={reconnectVegaWallet}>
|
||||
{t('Connect vega wallet')}
|
||||
</Button>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const isFinal = (tx: VegaStoredTxState) =>
|
||||
[VegaTxStatus.Error, VegaTxStatus.Complete].includes(tx.status);
|
||||
|
||||
export const useVegaTransactionToasts = () => {
|
||||
const vegaTransactions = useVegaTransactionStore((state) =>
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
);
|
||||
const dismissVegaTransaction = useVegaTransactionStore(
|
||||
(state) => state.dismiss
|
||||
);
|
||||
const [setToast, removeToast] = useToasts((store) => [
|
||||
store.setToast,
|
||||
store.remove,
|
||||
]);
|
||||
|
||||
const fromVegaTransaction = useCallback(
|
||||
(tx: VegaStoredTxState): Toast => {
|
||||
let content: ToastContent;
|
||||
if (tx.status === VegaTxStatus.Requested) {
|
||||
content = <VegaTxRequestedToastContent tx={tx} />;
|
||||
const [dismissTx, deleteTx] = useVegaTransactionStore((state) => [
|
||||
state.dismiss,
|
||||
state.delete,
|
||||
]);
|
||||
|
||||
const onClose = useCallback(
|
||||
(tx: VegaStoredTxState) => () => {
|
||||
const safeToDelete = isFinal(tx);
|
||||
if (safeToDelete) {
|
||||
deleteTx(tx.id);
|
||||
} else {
|
||||
dismissTx(tx.id);
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Pending) {
|
||||
content = <VegaTxPendingToastContentProps tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Complete) {
|
||||
content = <VegaTxCompleteToastsContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Error) {
|
||||
content = <VegaTxErrorToastContent tx={tx} />;
|
||||
}
|
||||
return {
|
||||
id: `vega-${tx.id}`,
|
||||
intent: getIntent(tx),
|
||||
onClose: () => dismissVegaTransaction(tx.id),
|
||||
loader: tx.status === VegaTxStatus.Pending,
|
||||
content,
|
||||
};
|
||||
removeToast(`vega-${tx.id}`);
|
||||
},
|
||||
[dismissVegaTransaction]
|
||||
[deleteTx, dismissTx, removeToast]
|
||||
);
|
||||
|
||||
const toasts = useMemo(() => {
|
||||
return [
|
||||
...compact(vegaTransactions)
|
||||
.filter((tx) => isTransactionTypeSupported(tx))
|
||||
.map(fromVegaTransaction),
|
||||
];
|
||||
}, [fromVegaTransaction, vegaTransactions]);
|
||||
const fromVegaTransaction = (tx: VegaStoredTxState): Toast => {
|
||||
let content: ToastContent;
|
||||
const closeAfter = isFinal(tx) ? CLOSE_AFTER : undefined;
|
||||
if (tx.status === VegaTxStatus.Requested) {
|
||||
content = <VegaTxRequestedToastContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Pending) {
|
||||
content = <VegaTxPendingToastContentProps tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Complete) {
|
||||
content = <VegaTxCompleteToastsContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Error) {
|
||||
content = <VegaTxErrorToastContent tx={tx} />;
|
||||
}
|
||||
|
||||
return toasts;
|
||||
// Transaction can be successful but the order can be rejected by the network
|
||||
const intent =
|
||||
tx.order && [OrderStatus.STATUS_REJECTED].includes(tx.order.status)
|
||||
? Intent.Danger
|
||||
: intentMap[tx.status];
|
||||
|
||||
return {
|
||||
id: `vega-${tx.id}`,
|
||||
intent,
|
||||
onClose: onClose(tx),
|
||||
loader: tx.status === VegaTxStatus.Pending,
|
||||
content,
|
||||
closeAfter,
|
||||
};
|
||||
};
|
||||
|
||||
useVegaTransactionStore.subscribe(
|
||||
(state) =>
|
||||
compact(
|
||||
state.transactions.filter(
|
||||
(tx) => tx?.dialogOpen && isTransactionTypeSupported(tx)
|
||||
)
|
||||
),
|
||||
(txs) => {
|
||||
txs.forEach((tx) => setToast(fromVegaTransaction(tx)));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws';
|
||||
import { DepositDialog } from '@vegaprotocol/deposits';
|
||||
import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3';
|
||||
import { WelcomeDialog } from '../components/welcome-dialog';
|
||||
import { TransferDialog } from '@vegaprotocol/accounts';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
|
||||
@@ -25,6 +26,7 @@ const DialogsContainer = () => {
|
||||
<DepositDialog />
|
||||
<Web3ConnectUncontrolledDialog />
|
||||
<CreateWithdrawalDialog />
|
||||
<TransferDialog />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
import { ToastsContainer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo } from 'react';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/governance';
|
||||
|
||||
import { useVegaTransactionToasts } from '../lib/hooks/use-vega-transaction-toasts';
|
||||
import { useEthereumTransactionToasts } from '../lib/hooks/use-ethereum-transaction-toasts';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '../lib/hooks/use-ethereum-withdraw-approval-toasts';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
const updateNetworkParametersToasts = useUpdateNetworkParametersToasts();
|
||||
const vegaTransactionToasts = useVegaTransactionToasts();
|
||||
const ethTransactionToasts = useEthereumTransactionToasts();
|
||||
const withdrawApprovalToasts = useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
const toasts = useMemo(() => {
|
||||
return sortBy(
|
||||
[
|
||||
...vegaTransactionToasts,
|
||||
...ethTransactionToasts,
|
||||
...withdrawApprovalToasts,
|
||||
...updateNetworkParametersToasts,
|
||||
],
|
||||
['createdBy']
|
||||
);
|
||||
}, [
|
||||
vegaTransactionToasts,
|
||||
ethTransactionToasts,
|
||||
withdrawApprovalToasts,
|
||||
updateNetworkParametersToasts,
|
||||
]);
|
||||
useUpdateNetworkParametersToasts();
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
const toasts = useToasts((store) => store.toasts);
|
||||
return <ToastsContainer order="desc" toasts={toasts} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './breakdown-table';
|
||||
export * from './use-account-balance';
|
||||
export * from './get-settlement-account';
|
||||
export * from './use-market-account-balance';
|
||||
export * from './transfer-dialog';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimal,
|
||||
NetworkParams,
|
||||
t,
|
||||
truncateByChars,
|
||||
useDataProvider,
|
||||
useNetworkParam,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { accountsDataProvider } from './accounts-data-provider';
|
||||
import { TransferForm } from './transfer-form';
|
||||
import { useTransferDialog } from './transfer-dialog';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const TransferContainer = () => {
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const open = useTransferDialog((store) => store.open);
|
||||
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: { partyId: pubKey },
|
||||
skip: !pubKey,
|
||||
});
|
||||
const create = useVegaTransactionStore((store) => store.create);
|
||||
|
||||
const transfer = useCallback(
|
||||
(transfer: Transfer) => {
|
||||
create({ transfer });
|
||||
open(false);
|
||||
},
|
||||
[create, open]
|
||||
);
|
||||
|
||||
const assets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data
|
||||
.filter(
|
||||
(account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||
)
|
||||
.map((account) => ({
|
||||
id: account.asset.id,
|
||||
symbol: account.asset.symbol,
|
||||
name: account.asset.name,
|
||||
decimals: account.asset.decimals,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
}));
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm mb-4">
|
||||
{t('Transfer funds to another Vega key from')}{' '}
|
||||
<Lozenge className="font-mono">{truncateByChars(pubKey || '')}</Lozenge>{' '}
|
||||
{t('If you are at all unsure, stop and seek advice.')}
|
||||
</p>
|
||||
<TransferForm
|
||||
pubKey={pubKey}
|
||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||
assets={assets}
|
||||
feeFactor={param}
|
||||
submitTransfer={transfer}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { create } from 'zustand';
|
||||
import { TransferContainer } from './transfer-container';
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
open: (open?: boolean) => void;
|
||||
}
|
||||
|
||||
export const useTransferDialog = create<State & Actions>((set) => ({
|
||||
isOpen: false,
|
||||
open: (open = true) => {
|
||||
set(() => ({ isOpen: open }));
|
||||
},
|
||||
}));
|
||||
|
||||
export const TransferDialog = () => {
|
||||
const { isOpen, open } = useTransferDialog();
|
||||
return (
|
||||
<Dialog title={t('Transfer')} open={isOpen} onChange={open} size="small">
|
||||
<TransferContainer />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { AddressField, TransferFee, TransferForm } from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { formatNumber, removeDecimal } from '@vegaprotocol/react-helpers';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
const submit = () => fireEvent.submit(screen.getByTestId('transfer-form'));
|
||||
const amount = '100';
|
||||
const pubKey =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
const asset = {
|
||||
id: 'asset-0',
|
||||
symbol: 'ASSET 0',
|
||||
name: 'Asset 0',
|
||||
decimals: 2,
|
||||
balance: '1000',
|
||||
};
|
||||
const props = {
|
||||
pubKey,
|
||||
pubKeys: [
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
assets: [asset],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
};
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||
expect(keySelect.children).toHaveLength(2);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||
'',
|
||||
props.pubKeys[1],
|
||||
]);
|
||||
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
|
||||
// Select a pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: props.pubKeys[1] },
|
||||
});
|
||||
|
||||
// Select asset
|
||||
fireEvent.change(
|
||||
// Bypass RichSelect and target hidden native select
|
||||
// eslint-disable-next-line
|
||||
document.querySelector('select[name="asset"]')!,
|
||||
{ target: { value: asset.id } }
|
||||
);
|
||||
|
||||
// assert rich select as updated
|
||||
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||
asset.name
|
||||
);
|
||||
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
|
||||
formatNumber(asset.balance, asset.decimals)
|
||||
);
|
||||
|
||||
// Test amount validation
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '0.00000001' },
|
||||
});
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: '9999999' },
|
||||
});
|
||||
expect(
|
||||
await screen.findByText(/cannot transfer more/i)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// set valid amount
|
||||
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||
target: { value: amount },
|
||||
});
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
||||
new BigNumber(props.feeFactor).times(amount).toFixed()
|
||||
);
|
||||
|
||||
submit();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('validates a manually entered address', async () => {
|
||||
render(<TransferForm {...props} />);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
fireEvent.click(toggle);
|
||||
// has switched to input
|
||||
expect(toggle).toHaveTextContent('Select from wallet');
|
||||
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: 'invalid-address' },
|
||||
});
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Invalid Vega key');
|
||||
});
|
||||
|
||||
// same pubkey
|
||||
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||
target: { value: pubKey },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errors = screen.getAllByTestId('input-error-text');
|
||||
expect(errors[0]).toHaveTextContent('Vega key is the same');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddressField', () => {
|
||||
const props = {
|
||||
pubKeys: ['pubkey-1', 'pubkey-2'],
|
||||
select: <div>select</div>,
|
||||
input: <div>input</div>,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
it('toggles content and calls onChange', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
render(<AddressField {...props} onChange={mockOnChange} />);
|
||||
|
||||
// select should be shown as multiple pubkeys provided
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Enter manually'));
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(screen.getByText('Select from wallet'));
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Does not provide select option if there is only a single key', () => {
|
||||
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TransferFee', () => {
|
||||
const props = {
|
||||
amount: '200',
|
||||
feeFactor: '0.001',
|
||||
};
|
||||
it('calculates and renders the transfer fee', () => {
|
||||
render(<TransferFee {...props} />);
|
||||
|
||||
const expected = new BigNumber(props.amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
import {
|
||||
t,
|
||||
minSafe,
|
||||
maxSafe,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputError,
|
||||
Option,
|
||||
RichSelect,
|
||||
Select,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
import { normalizeTransfer } from '@vegaprotocol/wallet';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
interface FormFields {
|
||||
toAddress: string;
|
||||
asset: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
interface TransferFormProps {
|
||||
pubKey: string | null;
|
||||
pubKeys: string[] | null;
|
||||
assets: Array<{
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
balance: string;
|
||||
}>;
|
||||
feeFactor: string | null;
|
||||
submitTransfer: (transfer: Transfer) => void;
|
||||
}
|
||||
|
||||
export const TransferForm = ({
|
||||
pubKey,
|
||||
pubKeys,
|
||||
assets,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
}: TransferFormProps) => {
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>();
|
||||
|
||||
const amount = watch('amount');
|
||||
const assetId = watch('asset');
|
||||
|
||||
const asset = useMemo(() => {
|
||||
return assets.find((a) => a.id === assetId);
|
||||
}, [assets, assetId]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
const transfer = normalizeTransfer(fields.toAddress, fields.amount, {
|
||||
id: asset.id,
|
||||
decimals: asset.decimals,
|
||||
});
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[asset, submitTransfer]
|
||||
);
|
||||
|
||||
const min = useMemo(() => {
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const minViableAmount = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
return minViableAmount;
|
||||
}, [asset]);
|
||||
|
||||
const max = useMemo(() => {
|
||||
const maxAmount = asset ? new BigNumber(asset.balance) : new BigNumber(0);
|
||||
return maxAmount;
|
||||
}, [asset]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="text-sm"
|
||||
data-testid="transfer-form"
|
||||
>
|
||||
<FormGroup label="Vega key" labelFor="to-address">
|
||||
<AddressField
|
||||
pubKeys={pubKeys}
|
||||
onChange={() => setValue('toAddress', '')}
|
||||
select={
|
||||
<Select {...register('toAddress')} id="to-address" defaultValue="">
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.length &&
|
||||
pubKeys
|
||||
.filter((pk) => pk !== pubKey) // remove currently selected pubkey
|
||||
.map((pk) => (
|
||||
<option key={pk} value={pk}>
|
||||
{pk}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
}
|
||||
input={
|
||||
<Input
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="to-address"
|
||||
type="text"
|
||||
{...register('toAddress', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
sameKey: (value) => {
|
||||
if (value === pubKey) {
|
||||
return t('Vega key is the same as current key');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.toAddress?.message && (
|
||||
<InputError forInput="to-address">
|
||||
{errors.toAddress.message}
|
||||
</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup label="Asset" labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<RichSelect
|
||||
data-testid="select-asset"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
placeholder={t('Please select')}
|
||||
value={field.value}
|
||||
>
|
||||
{assets.map((a) => (
|
||||
<Option key={a.id} value={a.id}>
|
||||
<div className="text-left" data-testid={`asset-${a.id}`}>
|
||||
<div>{a.name}</div>
|
||||
<div className="text-xs">
|
||||
<span className="font-mono" data-testid="asset-balance">
|
||||
{formatNumber(a.balance, a.decimals)}
|
||||
</span>{' '}
|
||||
<span>{a.symbol}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Option>
|
||||
))}
|
||||
</RichSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.asset?.message && (
|
||||
<InputError forInput="asset">{errors.asset.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup label="Amount" labelFor="amount">
|
||||
<Input
|
||||
id="amount"
|
||||
autoComplete="off"
|
||||
appendElement={
|
||||
asset && <span className="text-xs">{asset.symbol}</span>
|
||||
}
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(max)) {
|
||||
return t(
|
||||
'You cannot transfer more than your available collateral'
|
||||
);
|
||||
}
|
||||
return maxSafe(max)(v);
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.amount?.message && (
|
||||
<InputError forInput="amount">{errors.amount.message}</InputError>
|
||||
)}
|
||||
</FormGroup>
|
||||
<TransferFee amount={amount} feeFactor={feeFactor} />
|
||||
<Button type="submit" variant="primary" fill={true}>
|
||||
{t('Confirm transfer')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const TransferFee = ({
|
||||
amount,
|
||||
feeFactor,
|
||||
}: {
|
||||
amount: string;
|
||||
feeFactor: string | null;
|
||||
}) => {
|
||||
if (!feeFactor || !amount) return null;
|
||||
|
||||
// using toFixed without an argument will always return a
|
||||
// number in normal notation without rounding, formatting functions
|
||||
// arent working in a way which won't round the decimal places
|
||||
const value = new BigNumber(amount).times(feeFactor).toFixed();
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex justify-between items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<Tooltip
|
||||
description={t(
|
||||
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to ${feeFactor}`
|
||||
)}
|
||||
>
|
||||
<div>{t('Transfer fee')}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
data-testid="transfer-fee"
|
||||
className="text-neutral-500 dark:text-neutral-300"
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AddressInputProps {
|
||||
pubKeys: string[] | null;
|
||||
select: ReactNode;
|
||||
input: ReactNode;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const AddressField = ({
|
||||
pubKeys,
|
||||
select,
|
||||
input,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const [isInput, setIsInput] = useState(() => {
|
||||
if (pubKeys && pubKeys.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isInput ? input : select}
|
||||
{pubKeys && pubKeys.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="ml-auto text-sm absolute top-0 right-0 underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -10,9 +10,9 @@ interface Props {
|
||||
|
||||
export const DealTicketButton = ({ disabled, variant }: Props) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||
(store) => store.openVegaWalletDialog
|
||||
);
|
||||
return pubKey ? (
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
|
||||
@@ -31,8 +31,6 @@ import {
|
||||
usePersistedOrderStoreSubscription,
|
||||
} from '@vegaprotocol/orders';
|
||||
|
||||
export type TransactionStatus = 'default' | 'pending';
|
||||
|
||||
export interface DealTicketProps {
|
||||
market: MarketDealTicket;
|
||||
submit: (order: OrderSubmissionBody['orderSubmission']) => void;
|
||||
|
||||
@@ -7,13 +7,8 @@ fragment ProposalEventFields on Proposal {
|
||||
}
|
||||
|
||||
subscription ProposalEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Proposal]) {
|
||||
type
|
||||
event {
|
||||
... on Proposal {
|
||||
...ProposalEventFields
|
||||
}
|
||||
}
|
||||
proposals(partyId: $partyId) {
|
||||
...ProposalEventFields
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export type ProposalEventSubscriptionVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ProposalEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'AccountEvent' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote' } | { __typename?: 'Withdrawal' } }> | null };
|
||||
export type ProposalEventSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } };
|
||||
|
||||
export type UpdateNetworkParameterFieldsFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } } };
|
||||
|
||||
@@ -55,13 +55,8 @@ export const UpdateNetworkParameterFieldsFragmentDoc = gql`
|
||||
`;
|
||||
export const ProposalEventDocument = gql`
|
||||
subscription ProposalEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Proposal]) {
|
||||
type
|
||||
event {
|
||||
... on Proposal {
|
||||
...ProposalEventFields
|
||||
}
|
||||
}
|
||||
proposals(partyId: $partyId) {
|
||||
...ProposalEventFields
|
||||
}
|
||||
}
|
||||
${ProposalEventFieldsFragmentDoc}`;
|
||||
|
||||
@@ -28,24 +28,15 @@ export const useProposalEvent = (transaction: VegaTxState) => {
|
||||
variables: { partyId },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (!data?.busEvents?.length) {
|
||||
if (!data?.proposals) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No types available for the subscription result
|
||||
const matchingProposalEvent = data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== 'Proposal') {
|
||||
return false;
|
||||
}
|
||||
// typo in 'proposals' - this should be singular
|
||||
const proposal = data.proposals;
|
||||
|
||||
return e.event.id === id;
|
||||
});
|
||||
|
||||
if (
|
||||
matchingProposalEvent &&
|
||||
matchingProposalEvent.event.__typename === 'Proposal'
|
||||
) {
|
||||
callback(matchingProposalEvent.event);
|
||||
if (proposal.id === id) {
|
||||
callback(proposal);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { UpdateNetworkParameter } from '@vegaprotocol/types';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import compact from 'lodash/compact';
|
||||
@@ -28,7 +29,7 @@ const UpdateNetworkParameterToastContent = ({
|
||||
const enactment = Date.parse(proposal.terms.enactmentDatetime);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{title}</h3>
|
||||
<ToastHeading>{title}</ToastHeading>
|
||||
<p className="italic">
|
||||
'
|
||||
{t(
|
||||
@@ -52,9 +53,8 @@ const UpdateNetworkParameterToastContent = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateNetworkParametersToasts = (): Toast[] => {
|
||||
const { proposalToasts, setToast, remove } = useToasts((store) => ({
|
||||
proposalToasts: store.toasts,
|
||||
export const useUpdateNetworkParametersToasts = () => {
|
||||
const { setToast, remove } = useToasts((store) => ({
|
||||
setToast: store.setToast,
|
||||
remove: store.remove,
|
||||
}));
|
||||
@@ -66,7 +66,9 @@ export const useUpdateNetworkParametersToasts = (): Toast[] => {
|
||||
id: `update-network-param-proposal-${proposal.id}`,
|
||||
intent: Intent.Warning,
|
||||
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
|
||||
onClose: () => remove(id),
|
||||
onClose: () => {
|
||||
remove(id);
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
},
|
||||
@@ -96,6 +98,4 @@ export const useUpdateNetworkParametersToasts = (): Toast[] => {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return proposalToasts;
|
||||
};
|
||||
|
||||
+14
-21
@@ -1,6 +1,6 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useUpdateNetworkParametersToasts } from './use-update-network-paramaters-toasts';
|
||||
@@ -9,8 +9,8 @@ import type {
|
||||
OnUpdateNetworkParametersSubscription,
|
||||
} from './__generated__/Proposal';
|
||||
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
|
||||
import waitForNextTick from 'flush-promises';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
const render = (mocks?: MockedResponse[]) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
@@ -92,11 +92,10 @@ const mockedEvent: MockedResponse<OnUpdateNetworkParametersSubscription> = {
|
||||
},
|
||||
};
|
||||
|
||||
const INITIAL = useToasts.getState();
|
||||
|
||||
const clear = () => {
|
||||
const { result: clearer } = renderHook(() =>
|
||||
useToasts((store) => store.removeAll)
|
||||
);
|
||||
act(() => clearer.current());
|
||||
useToasts.setState(INITIAL);
|
||||
};
|
||||
|
||||
describe('useUpdateNetworkParametersToasts', () => {
|
||||
@@ -104,29 +103,23 @@ describe('useUpdateNetworkParametersToasts', () => {
|
||||
afterAll(clear);
|
||||
|
||||
it('returns toast for update network parameters bus event', async () => {
|
||||
const { waitForNextUpdate, result } = render([mockedEvent]);
|
||||
await act(async () => {
|
||||
waitForNextUpdate();
|
||||
await waitForNextTick();
|
||||
render([mockedEvent]);
|
||||
await waitFor(() => {
|
||||
expect(useToasts.getState().count).toBe(1);
|
||||
});
|
||||
expect(result.current.length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not return toast for empty event', async () => {
|
||||
const { waitForNextUpdate, result } = render([mockedEmptyEvent]);
|
||||
await act(async () => {
|
||||
waitForNextUpdate();
|
||||
await waitForNextTick();
|
||||
render([mockedEmptyEvent]);
|
||||
await waitFor(() => {
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
expect(result.current.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not return toast for wrong event', async () => {
|
||||
const { waitForNextUpdate, result } = render([mockedWrongEvent]);
|
||||
await act(async () => {
|
||||
waitForNextUpdate();
|
||||
await waitForNextTick();
|
||||
render([mockedWrongEvent]);
|
||||
await waitFor(() => {
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
expect(result.current.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
fragment VoteEventFields on Vote {
|
||||
fragment VoteEventFields on ProposalVote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
vote {
|
||||
value
|
||||
datetime
|
||||
}
|
||||
}
|
||||
|
||||
subscription VoteEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
|
||||
type
|
||||
event {
|
||||
... on Vote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
}
|
||||
}
|
||||
votes(partyId: $partyId) {
|
||||
...VoteEventFields
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,36 +3,31 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type VoteEventFieldsFragment = { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: any };
|
||||
export type VoteEventFieldsFragment = { __typename?: 'ProposalVote', proposalId: string, vote: { __typename?: 'Vote', value: Types.VoteValue, datetime: any } };
|
||||
|
||||
export type VoteEventSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type VoteEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'AccountEvent' } | { __typename?: 'Asset' } | { __typename?: 'AuctionEvent' } | { __typename?: 'Deposit' } | { __typename?: 'LiquidityProvision' } | { __typename?: 'LossSocialization' } | { __typename?: 'MarginLevels' } | { __typename?: 'Market' } | { __typename?: 'MarketData' } | { __typename?: 'MarketEvent' } | { __typename?: 'MarketTick' } | { __typename?: 'NodeSignature' } | { __typename?: 'OracleSpec' } | { __typename?: 'Order' } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal' } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote', proposalId: string, value: Types.VoteValue, datetime: any } | { __typename?: 'Withdrawal' } }> | null };
|
||||
export type VoteEventSubscription = { __typename?: 'Subscription', votes: { __typename?: 'ProposalVote', proposalId: string, vote: { __typename?: 'Vote', value: Types.VoteValue, datetime: any } } };
|
||||
|
||||
export const VoteEventFieldsFragmentDoc = gql`
|
||||
fragment VoteEventFields on Vote {
|
||||
fragment VoteEventFields on ProposalVote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
vote {
|
||||
value
|
||||
datetime
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const VoteEventDocument = gql`
|
||||
subscription VoteEvent($partyId: ID!) {
|
||||
busEvents(partyId: $partyId, batchSize: 0, types: [Vote]) {
|
||||
type
|
||||
event {
|
||||
... on Vote {
|
||||
proposalId
|
||||
value
|
||||
datetime
|
||||
}
|
||||
}
|
||||
votes(partyId: $partyId) {
|
||||
...VoteEventFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
${VoteEventFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useVoteEventSubscription__
|
||||
|
||||
@@ -25,23 +25,14 @@ export const useVoteEvent = (transaction: VegaTxState) => {
|
||||
variables: { partyId },
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
if (!data?.busEvents?.length) {
|
||||
if (!data?.votes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingVoteEvent = data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== 'Vote') {
|
||||
return false;
|
||||
}
|
||||
const vote = data.votes;
|
||||
|
||||
return e.event.proposalId === proposalId;
|
||||
});
|
||||
|
||||
if (
|
||||
matchingVoteEvent &&
|
||||
matchingVoteEvent.event.__typename === 'Vote'
|
||||
) {
|
||||
callback(matchingVoteEvent.event);
|
||||
if (vote.proposalId === proposalId) {
|
||||
callback(vote);
|
||||
subRef.current?.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { NetworkParamsKey } from './use-network-params';
|
||||
import { toRealKey } from './use-network-params';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParam,
|
||||
@@ -18,7 +19,7 @@ describe('useNetworkParam', () => {
|
||||
request: {
|
||||
query: NetworkParamDocument,
|
||||
variables: {
|
||||
key: arg,
|
||||
key: toRealKey(arg),
|
||||
},
|
||||
},
|
||||
result: {
|
||||
|
||||
@@ -104,10 +104,11 @@ export const NetworkParams = {
|
||||
market_liquidity_stakeToCcyVolume: 'market_liquidity_stakeToCcyVolume',
|
||||
market_liquidity_targetstake_triggering_ratio:
|
||||
'market_liquidity_targetstake_triggering_ratio',
|
||||
transfer_fee_factor: 'transfer_fee_factor',
|
||||
} as const;
|
||||
|
||||
type Params = typeof NetworkParams;
|
||||
export type NetworkParamsKey = Params[keyof Params];
|
||||
export type NetworkParamsKey = keyof Params;
|
||||
type Result = {
|
||||
[key in keyof Params]: string;
|
||||
};
|
||||
@@ -120,7 +121,7 @@ export const useNetworkParams = <T extends NetworkParamsKey[]>(params?: T) => {
|
||||
return compact(data.networkParametersConnection.edges)
|
||||
.map((p) => ({
|
||||
...p.node,
|
||||
key: p.node.key.split('.').join('_'),
|
||||
key: toInternalKey(p.node.key),
|
||||
}))
|
||||
.filter((p) => {
|
||||
if (params === undefined || params.length === 0) return true;
|
||||
@@ -143,7 +144,7 @@ export const useNetworkParams = <T extends NetworkParamsKey[]>(params?: T) => {
|
||||
export const useNetworkParam = (param: NetworkParamsKey) => {
|
||||
const { data, loading, error } = useNetworkParamQuery({
|
||||
variables: {
|
||||
key: param,
|
||||
key: toRealKey(param),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -153,3 +154,11 @@ export const useNetworkParam = (param: NetworkParamsKey) => {
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const toRealKey = (key: NetworkParamsKey) => {
|
||||
return key.split('_').join('.');
|
||||
};
|
||||
|
||||
export const toInternalKey = (key: string) => {
|
||||
return key.split('.').join('_');
|
||||
};
|
||||
|
||||
@@ -174,11 +174,16 @@ module.exports = {
|
||||
'60%': { transform: 'rotate( 0.0deg)' },
|
||||
'100%': { transform: 'rotate( 0.0deg)' },
|
||||
},
|
||||
progress: {
|
||||
from: { width: '0' },
|
||||
to: { width: '100%' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
rotate: 'rotate 2s linear alternate infinite',
|
||||
'rotate-back': 'rotate 2s linear reverse infinite',
|
||||
wave: 'wave 2s linear infinite',
|
||||
progress: 'progress 5s cubic-bezier(.39,.58,.57,1) 1',
|
||||
},
|
||||
data: {
|
||||
selected: 'state~="checked"',
|
||||
|
||||
@@ -151,7 +151,7 @@ export const DropdownMenuSeparator = forwardRef<
|
||||
{...separatorProps}
|
||||
ref={forwardedRef}
|
||||
className={classNames(
|
||||
'h-px my-1 mx-2 bg-neutral-700 dark:bg-black',
|
||||
'h-px my-1 mx-2 bg-neutral-400 dark:bg-neutral-300',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ProgressBarProps {
|
||||
export const ProgressBar = ({ className, intent, value }: ProgressBarProps) => {
|
||||
return (
|
||||
<div
|
||||
data-progress-bar
|
||||
style={{ height: '6px' }}
|
||||
className={classNames(
|
||||
'bg-neutral-300 dark:bg-neutral-700 relative',
|
||||
@@ -18,6 +19,7 @@ export const ProgressBar = ({ className, intent, value }: ProgressBarProps) => {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-progress-bar-value
|
||||
className={classNames(
|
||||
'absolute left-0 top-0 bottom-0',
|
||||
intent === undefined || intent === Intent.None
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
|
||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ className, hasError, ...props }, ref) => (
|
||||
<div className="flex items-center relative">
|
||||
<div className="relative">
|
||||
<select
|
||||
ref={ref}
|
||||
{...props}
|
||||
@@ -25,7 +25,10 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
'appearance-none rounded-md'
|
||||
)}
|
||||
/>
|
||||
<Icon name="chevron-down" className="absolute right-4 z-10" />
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
className="absolute top-3 right-4 z-10 pointer-events-none"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
.initial {
|
||||
top: 20px;
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
border: 0;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.showing {
|
||||
right: 0;
|
||||
opacity: 1;
|
||||
transition: all 0.3s;
|
||||
max-height: 100vw;
|
||||
}
|
||||
|
||||
.expired {
|
||||
right: -375px;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
max-height: 0;
|
||||
transition: all 0.75s;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable jsx-a11y/accessible-emoji */
|
||||
import { Toast } from './toast';
|
||||
import { Panel, Toast, ToastHeading } from './toast';
|
||||
import type { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { Intent } from '../../utils/intent';
|
||||
import { ExternalLink } from '../link';
|
||||
import { ProgressBar } from '../progress-bar';
|
||||
|
||||
export default {
|
||||
title: 'Toast',
|
||||
@@ -9,14 +11,7 @@ export default {
|
||||
} as ComponentMeta<typeof Toast>;
|
||||
|
||||
const Template: ComponentStory<typeof Toast> = (args) => {
|
||||
const toastContent = (
|
||||
<>
|
||||
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
|
||||
<p>Eaque exercitationem saepe cupiditate sunt impedit.</p>
|
||||
<p>I really like 🥪🥪🥪!</p>
|
||||
</>
|
||||
);
|
||||
return <Toast {...args} content={toastContent} />;
|
||||
return <Toast {...args} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
@@ -24,6 +19,16 @@ Default.args = {
|
||||
id: 'def',
|
||||
intent: Intent.None,
|
||||
state: 'showing',
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>Optional heading</ToastHeading>
|
||||
<p>This is a message that can return over multiple lines.</p>
|
||||
<p>
|
||||
<ExternalLink>Optional link</ExternalLink>
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
onClose: () => undefined,
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
@@ -31,6 +36,17 @@ Primary.args = {
|
||||
id: 'pri',
|
||||
intent: Intent.Primary,
|
||||
state: 'showing',
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>Optional heading</ToastHeading>
|
||||
<p>This is a message that can return over multiple lines.</p>
|
||||
<p>
|
||||
<ExternalLink>Optional link</ExternalLink>
|
||||
</p>
|
||||
<Panel>Lorem ipsum dolor sit amet consectetur adipisicing elit</Panel>
|
||||
</>
|
||||
),
|
||||
onClose: () => undefined,
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
@@ -38,6 +54,17 @@ Danger.args = {
|
||||
id: 'dan',
|
||||
intent: Intent.Danger,
|
||||
state: 'showing',
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>Optional heading</ToastHeading>
|
||||
<p>This is a message that can return over multiple lines.</p>
|
||||
<p>
|
||||
<ExternalLink>Optional link</ExternalLink>
|
||||
</p>
|
||||
<Panel>Lorem ipsum dolor sit amet consectetur adipisicing elit</Panel>
|
||||
</>
|
||||
),
|
||||
onClose: () => undefined,
|
||||
};
|
||||
|
||||
export const Warning = Template.bind({});
|
||||
@@ -45,6 +72,21 @@ Warning.args = {
|
||||
id: 'war',
|
||||
intent: Intent.Warning,
|
||||
state: 'showing',
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>Optional heading</ToastHeading>
|
||||
<p>This is a message that can return over multiple lines.</p>
|
||||
<p>
|
||||
<ExternalLink>Optional link</ExternalLink>
|
||||
</p>
|
||||
<Panel>
|
||||
<strong>Deposit 10.00 tUSDX</strong>
|
||||
<p className="mt-[2px]">Awaiting confirmations (1/3)</p>
|
||||
<ProgressBar value={33.33} />
|
||||
</Panel>
|
||||
</>
|
||||
),
|
||||
onClose: () => undefined,
|
||||
};
|
||||
|
||||
export const Success = Template.bind({});
|
||||
@@ -52,4 +94,15 @@ Success.args = {
|
||||
id: 'suc',
|
||||
intent: Intent.Success,
|
||||
state: 'showing',
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>Optional heading</ToastHeading>
|
||||
<p>This is a message that can return over multiple lines.</p>
|
||||
<p>
|
||||
<ExternalLink>Optional link</ExternalLink>
|
||||
</p>
|
||||
<Panel>Lorem ipsum dolor sit amet consectetur adipisicing elit</Panel>
|
||||
</>
|
||||
),
|
||||
onClose: () => undefined,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import styles from './toast.module.css';
|
||||
import type { IconName } from '@blueprintjs/icons';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect } from 'react';
|
||||
import type { HTMLAttributes, HtmlHTMLAttributes } from 'react';
|
||||
import { forwardRef, useEffect } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useRef } from 'react';
|
||||
@@ -33,20 +34,43 @@ const toastIconMapping: { [i in Intent]: IconName } = {
|
||||
[Intent.None]: IconNames.HELP,
|
||||
[Intent.Primary]: IconNames.INFO_SIGN,
|
||||
[Intent.Success]: IconNames.TICK_CIRCLE,
|
||||
[Intent.Warning]: IconNames.ERROR,
|
||||
[Intent.Warning]: IconNames.WARNING_SIGN,
|
||||
[Intent.Danger]: IconNames.ERROR,
|
||||
};
|
||||
|
||||
const getToastAccent = (intent: Intent) => ({
|
||||
// strip
|
||||
'bg-gray-200 text-black text-opacity-70': intent === Intent.None,
|
||||
'bg-vega-blue text-white text-opacity-70': intent === Intent.Primary,
|
||||
'bg-success text-white text-opacity-70': intent === Intent.Success,
|
||||
'bg-warning text-white text-opacity-70': intent === Intent.Warning,
|
||||
'bg-vega-pink text-white text-opacity-70': intent === Intent.Danger,
|
||||
});
|
||||
export const CLOSE_DELAY = 500;
|
||||
export const TICKER = 100;
|
||||
export const CLOSE_AFTER = 5000;
|
||||
|
||||
export const CLOSE_DELAY = 750;
|
||||
export const Panel = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
data-panel
|
||||
ref={ref}
|
||||
data-testid="toast-panel"
|
||||
className={classNames(
|
||||
'p-2 rounded mt-[10px]',
|
||||
'font-mono text-[12px] leading-[16px] font-normal',
|
||||
'[&>h4]:font-bold',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const ToastHeading = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
HtmlHTMLAttributes<HTMLHeadingElement>
|
||||
>(({ children, ...props }, ref) => (
|
||||
<h3 ref={ref} className="text-sm uppercase mb-1" {...props}>
|
||||
{children}
|
||||
</h3>
|
||||
));
|
||||
|
||||
export const Toast = ({
|
||||
id,
|
||||
@@ -59,6 +83,9 @@ export const Toast = ({
|
||||
loader = false,
|
||||
}: ToastProps) => {
|
||||
const toastRef = useRef<HTMLDivElement>(null);
|
||||
const progressRef = useRef<HTMLDivElement>(null);
|
||||
const ticker = useRef<number>(0);
|
||||
const lock = useRef<boolean>(false);
|
||||
|
||||
const closeToast = useCallback(() => {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -80,16 +107,21 @@ export const Toast = ({
|
||||
}
|
||||
});
|
||||
return () => cancelAnimationFrame(req);
|
||||
}, [id]);
|
||||
}, [id, intent, content]); // DO NOT REMOVE DEPS: intent, content
|
||||
|
||||
useEffect(() => {
|
||||
let t: NodeJS.Timeout;
|
||||
if (closeAfter && closeAfter > 0) {
|
||||
t = setTimeout(() => {
|
||||
const i = setInterval(() => {
|
||||
if (!closeAfter || closeAfter === 0) return;
|
||||
if (!lock.current) {
|
||||
ticker.current += 100;
|
||||
}
|
||||
if (ticker.current >= closeAfter) {
|
||||
closeToast();
|
||||
}, closeAfter);
|
||||
}
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, 100);
|
||||
return () => {
|
||||
clearInterval(i);
|
||||
};
|
||||
}, [closeAfter, closeToast]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,14 +130,76 @@ export const Toast = ({
|
||||
}
|
||||
}, [closeToast, signal]);
|
||||
|
||||
const withProgress = Boolean(closeAfter && closeAfter > 0);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<div
|
||||
data-testid="toast"
|
||||
data-toast-id={id}
|
||||
ref={toastRef}
|
||||
role="dialog"
|
||||
onMouseLeave={() => {
|
||||
lock.current = false;
|
||||
if (progressRef.current) {
|
||||
progressRef.current.style.animationPlayState = 'running';
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
lock.current = true;
|
||||
if (progressRef.current) {
|
||||
progressRef.current.style.animationPlayState = 'paused';
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
'relative w-[300px] top-0 rounded-md border overflow-hidden mb-2',
|
||||
'text-black bg-white dark:border-zinc-700',
|
||||
'w-[320px] rounded-md overflow-hidden',
|
||||
'shadow-[8px_8px_16px_0_rgba(0,0,0,0.4)]',
|
||||
'text-black dark:text-white',
|
||||
'font-alpha liga-0-calt-0 text-[14px] leading-[19px]',
|
||||
// background
|
||||
{
|
||||
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
|
||||
'bg-vega-blue-300 dark:bg-vega-blue-700': intent === Intent.Primary,
|
||||
'bg-vega-green-300 dark:bg-vega-green-700': intent === Intent.Success,
|
||||
'bg-vega-orange-300 dark:bg-vega-orange-700':
|
||||
intent === Intent.Warning,
|
||||
'bg-vega-pink-300 dark:bg-vega-pink-700': intent === Intent.Danger,
|
||||
},
|
||||
// panel's colours
|
||||
{
|
||||
'[&_[data-panel]]:bg-vega-light-150 [&_[data-panel]]:dark:bg-vega-dark-150 ':
|
||||
intent === Intent.None,
|
||||
'[&_[data-panel]]:bg-vega-blue-350 [&_[data-panel]]:dark:bg-vega-blue-650':
|
||||
intent === Intent.Primary,
|
||||
'[&_[data-panel]]:bg-vega-green-350 [&_[data-panel]]:dark:bg-vega-green-650':
|
||||
intent === Intent.Success,
|
||||
'[&_[data-panel]]:bg-vega-orange-350 [&_[data-panel]]:dark:bg-vega-orange-650':
|
||||
intent === Intent.Warning,
|
||||
'[&_[data-panel]]:bg-vega-pink-350 [&_[data-panel]]:dark:bg-vega-pink-650':
|
||||
intent === Intent.Danger,
|
||||
},
|
||||
// panels's progress bar colours
|
||||
'[&_[data-progress-bar]]:mt-[10px] [&_[data-progress-bar]]:mb-[4px]',
|
||||
{
|
||||
'[&_[data-progress-bar]]:bg-vega-light-200 [&_[data-progress-bar]]:dark:bg-vega-dark-200 ':
|
||||
intent === Intent.None,
|
||||
'[&_[data-progress-bar]]:bg-vega-blue-400 [&_[data-progress-bar]]:dark:bg-vega-blue-600':
|
||||
intent === Intent.Primary,
|
||||
'[&_[data-progress-bar-value]]:bg-vega-blue-500 [&_[data-progress-bar-value]]:dark:bg-vega-blue-500':
|
||||
intent === Intent.Primary,
|
||||
'[&_[data-progress-bar]]:bg-vega-green-400 [&_[data-progress-bar]]:dark:bg-vega-green-600':
|
||||
intent === Intent.Success,
|
||||
'[&_[data-progress-bar-value]]:bg-vega-green-600 [&_[data-progress-bar-value]]:dark:bg-vega-green-500':
|
||||
intent === Intent.Success,
|
||||
'[&_[data-progress-bar]]:bg-vega-orange-400 [&_[data-progress-bar]]:dark:bg-vega-orange-600':
|
||||
intent === Intent.Warning,
|
||||
'[&_[data-progress-bar-value]]:bg-vega-orange-500 [&_[data-progress-bar-value]]:dark:bg-vega-orange-500':
|
||||
intent === Intent.Warning,
|
||||
'[&_[data-progress-bar]]:bg-vega-pink-400 [&_[data-progress-bar]]:dark:bg-vega-pink-600':
|
||||
intent === Intent.Danger,
|
||||
'[&_[data-progress-bar-value]]:bg-vega-pink-500 [&_[data-progress-bar-value]]:dark:bg-vega-pink-500':
|
||||
intent === Intent.Danger,
|
||||
},
|
||||
{
|
||||
[styles['initial']]: state === 'initial',
|
||||
[styles['showing']]: state === 'showing',
|
||||
@@ -118,26 +212,81 @@ export const Toast = ({
|
||||
type="button"
|
||||
data-testid="toast-close"
|
||||
onClick={closeToast}
|
||||
className="absolute p-2 top-0 right-0"
|
||||
className="absolute p-[8px] top-[3px] right-[3px] z-20"
|
||||
>
|
||||
<Icon name="cross" size={3} className="!block dark:text-white" />
|
||||
<Icon
|
||||
name="cross"
|
||||
size={3}
|
||||
className="!block dark:text-white !w-[11px] !h-[11px]"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={classNames(getToastAccent(intent), 'p-2 pt-3 text-center')}
|
||||
data-testid="toast-accent"
|
||||
className={classNames(
|
||||
{
|
||||
// gray
|
||||
'bg-vega-light-200 dark:bg-vega-dark-200 text-vega-light-400 dark:text-vega-dark-100':
|
||||
intent === Intent.None,
|
||||
// blue
|
||||
'bg-vega-blue-500 text-vega-blue-600': intent === Intent.Primary,
|
||||
// green
|
||||
'bg-vega-green-500 text-vega-green-600':
|
||||
intent === Intent.Success,
|
||||
// orange
|
||||
'bg-vega-orange-500 text-vega-orange-600':
|
||||
intent === Intent.Warning,
|
||||
// pink
|
||||
'bg-vega-pink-500 text-vega-pink-600': intent === Intent.Danger,
|
||||
},
|
||||
'w-8 p-[9px]',
|
||||
'flex justify-center'
|
||||
)}
|
||||
>
|
||||
{loader ? (
|
||||
<div className="w-4 h-4">
|
||||
<div className="w-[15px] h-[15px]">
|
||||
<Loader size="small" forceTheme="dark" />
|
||||
</div>
|
||||
) : (
|
||||
<Icon name={toastIconMapping[intent]} size={4} className="!block" />
|
||||
<Icon
|
||||
name={toastIconMapping[intent]}
|
||||
size={4}
|
||||
className="!block !w-[14px] !h-[14px]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex-1 p-2 pr-6 text-sm overflow-auto dark:bg-black dark:text-white"
|
||||
className={classNames(
|
||||
'relative',
|
||||
'overflow-auto flex-1 p-4 pr-[40px] [&>p]:mb-[2.5px]'
|
||||
)}
|
||||
data-testid="toast-content"
|
||||
>
|
||||
{content}
|
||||
{withProgress && (
|
||||
<div
|
||||
ref={progressRef}
|
||||
data-testid="toast-progress-bar"
|
||||
className={classNames(
|
||||
{
|
||||
'bg-vega-light-200 dark:bg-vega-dark-200 ':
|
||||
intent === Intent.None,
|
||||
'bg-vega-blue-400 dark:bg-vega-blue-600':
|
||||
intent === Intent.Primary,
|
||||
'bg-vega-green-400 dark:bg-vega-green-600':
|
||||
intent === Intent.Success,
|
||||
'bg-vega-orange-400 dark:bg-vega-orange-600':
|
||||
intent === Intent.Warning,
|
||||
'bg-vega-pink-400 dark:bg-vega-pink-600':
|
||||
intent === Intent.Danger,
|
||||
},
|
||||
'absolute bottom-0 left-0 w-full h-[4px]',
|
||||
'animate-progress'
|
||||
)}
|
||||
style={{
|
||||
animationDuration: `${closeAfter}ms`,
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { act, render, renderHook, screen } from '@testing-library/react';
|
||||
import { ToastsContainer, useToasts } from '..';
|
||||
import { CLOSE_DELAY, ToastsContainer, useToasts } from '..';
|
||||
import { Intent } from '../../utils/intent';
|
||||
|
||||
describe('ToastsContainer', () => {
|
||||
@@ -108,7 +108,7 @@ describe('ToastsContainer', () => {
|
||||
) as HTMLButtonElement;
|
||||
act(() => {
|
||||
closeBtn.click();
|
||||
jest.runAllTimers();
|
||||
jest.advanceTimersByTime(CLOSE_DELAY);
|
||||
});
|
||||
rerender(<ToastsContainer order="asc" toasts={result.current.toasts} />);
|
||||
const toasts = [...screen.queryAllByTestId('toast-content')].map((t) =>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { Intent } from '../../utils/intent';
|
||||
import type { Toast } from './toast';
|
||||
import { ToastHeading } from './toast';
|
||||
import { ToastsContainer } from './toasts-container';
|
||||
import random from 'lodash/random';
|
||||
import sample from 'lodash/sample';
|
||||
@@ -59,7 +60,8 @@ const randomWords = [
|
||||
];
|
||||
|
||||
const randomToast = (): Toast => {
|
||||
const content = sample(contents);
|
||||
const now = new Date().toISOString();
|
||||
const content = now + ' ' + sample(contents);
|
||||
return {
|
||||
id: String(uniqueId('toast_')),
|
||||
intent: sample<Intent>([
|
||||
@@ -83,7 +85,7 @@ const usePrice = create<PriceStore>((set) => ({
|
||||
const Template: ComponentStory<typeof ToastsContainer> = (args) => {
|
||||
const setPrice = usePrice((state) => state.setPrice);
|
||||
|
||||
const { add, close, closeAll, update, remove, toasts } = useToasts(
|
||||
const { add, close, closeAll, update, remove, toasts, setToast } = useToasts(
|
||||
(state) => ({
|
||||
add: state.add,
|
||||
close: state.close,
|
||||
@@ -91,6 +93,7 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
|
||||
update: state.update,
|
||||
remove: state.remove,
|
||||
toasts: state.toasts,
|
||||
setToast: state.setToast,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -195,6 +198,26 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
|
||||
>
|
||||
🧽
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const toasts = Object.values(useToasts.getState().toasts);
|
||||
if (toasts.length > 0) {
|
||||
const t = toasts[toasts.length - 1];
|
||||
setToast({
|
||||
...t,
|
||||
intent: Intent.Danger,
|
||||
content: (
|
||||
<>
|
||||
<ToastHeading>Error occurred</ToastHeading>
|
||||
<p>Something went terribly wrong</p>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Set first as Error
|
||||
</button>
|
||||
<ToastsContainer {...args} toasts={toasts} />
|
||||
</div>
|
||||
);
|
||||
@@ -202,5 +225,5 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
order: 'asc',
|
||||
order: 'desc',
|
||||
};
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { t, usePrevious } from '@vegaprotocol/react-helpers';
|
||||
import classNames from 'classnames';
|
||||
import type { Ref } from 'react';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { Button } from '../button';
|
||||
import { Toast } from './toast';
|
||||
import type { Toasts } from './use-toasts';
|
||||
import { useToasts } from './use-toasts';
|
||||
|
||||
import { Portal } from '@radix-ui/react-portal';
|
||||
|
||||
type ToastsContainerProps = {
|
||||
toasts: Toast[];
|
||||
toasts: Toasts;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
@@ -10,23 +18,75 @@ export const ToastsContainer = ({
|
||||
toasts,
|
||||
order = 'asc',
|
||||
}: ToastsContainerProps) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const closeAll = useToasts((store) => store.closeAll);
|
||||
|
||||
// Scroll to top for desc, bottom for asc when a toast is added.
|
||||
const count = usePrevious(Object.keys(toasts).length) || 0;
|
||||
useLayoutEffect(() => {
|
||||
const t = setTimeout(
|
||||
() => {
|
||||
if (Object.keys(toasts).length > count) {
|
||||
ref.current?.scrollTo({
|
||||
top: order === 'desc' ? 0 : ref.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
},
|
||||
300 // need to delay scroll down in order for the toast to appear
|
||||
);
|
||||
return () => {
|
||||
clearTimeout(t);
|
||||
};
|
||||
}, [count, order, toasts]);
|
||||
|
||||
return (
|
||||
<ul
|
||||
<Portal
|
||||
ref={ref as Ref<HTMLDivElement>}
|
||||
className={classNames(
|
||||
'absolute top-0 right-0 pt-2 pr-2 max-w-full z-20 max-h-full overflow-x-hidden overflow-y-auto',
|
||||
'group',
|
||||
'absolute bottom-0 right-0 z-20 ',
|
||||
'p-[8px_16px_16px_16px]',
|
||||
'max-w-full max-h-full overflow-x-hidden overflow-y-auto',
|
||||
{
|
||||
'flex flex-col-reverse': order === 'desc',
|
||||
hidden: Object.keys(toasts).length === 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{toasts &&
|
||||
toasts.map((toast) => {
|
||||
return (
|
||||
<li key={toast.id}>
|
||||
<Toast {...toast} />
|
||||
</li>
|
||||
);
|
||||
<ul
|
||||
className={classNames('relative mt-[38px]', 'flex flex-col gap-[8px]', {
|
||||
'flex-col-reverse': order === 'desc',
|
||||
})}
|
||||
</ul>
|
||||
>
|
||||
{toasts &&
|
||||
Object.values(toasts).map((toast) => {
|
||||
return (
|
||||
<li key={toast.id}>
|
||||
<Toast {...toast} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
title={t('Dismiss all toasts')}
|
||||
size="sm"
|
||||
fill={true}
|
||||
className={classNames(
|
||||
'absolute top-[-38px] right-0 z-20',
|
||||
'transition-opacity',
|
||||
'opacity-0 group-hover:opacity-50 hover:!opacity-100',
|
||||
'text-sm text-black dark:text-white bg-white dark:bg-black hover:!bg-white hover:dark:!bg-black',
|
||||
{
|
||||
hidden: Object.keys(toasts).length === 0,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
closeAll();
|
||||
}}
|
||||
variant={'default'}
|
||||
>
|
||||
{t('Dismiss all')}
|
||||
</Button>
|
||||
</ul>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Intent } from '../../utils/intent';
|
||||
import type { Toast } from './toast';
|
||||
import { useToasts } from './use-toasts';
|
||||
|
||||
const T1: Toast = {
|
||||
id: 'TEST-1',
|
||||
intent: Intent.None,
|
||||
content: undefined,
|
||||
};
|
||||
|
||||
const T2: Toast = {
|
||||
id: 'TEST-2',
|
||||
intent: Intent.None,
|
||||
content: undefined,
|
||||
};
|
||||
|
||||
const T3: Toast = {
|
||||
id: 'TEST-3',
|
||||
intent: Intent.None,
|
||||
content: undefined,
|
||||
};
|
||||
|
||||
const INITIAL = useToasts.getState();
|
||||
|
||||
describe('useToasts', () => {
|
||||
beforeEach(() => {
|
||||
useToasts.setState(INITIAL, true);
|
||||
});
|
||||
afterAll(() => {
|
||||
useToasts.setState(INITIAL, true);
|
||||
});
|
||||
|
||||
it('adds toast', () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
act(() => {
|
||||
result.current.add(T1);
|
||||
});
|
||||
expect(result.current.toasts[T1.id]).toEqual(T1);
|
||||
expect(result.current.count).toEqual(1);
|
||||
});
|
||||
|
||||
it('removes toast', () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
act(() => {
|
||||
result.current.add(T1);
|
||||
result.current.add(T2);
|
||||
result.current.add(T3);
|
||||
result.current.remove(T1.id);
|
||||
result.current.remove(T1.id);
|
||||
result.current.remove(T1.id);
|
||||
});
|
||||
expect(result.current.toasts[T1.id]).toBeUndefined();
|
||||
expect(result.current.count).toEqual(2);
|
||||
});
|
||||
|
||||
it('updates toast', () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
const data = { content: <p>Burning hot toast</p> };
|
||||
act(() => {
|
||||
result.current.add(T1);
|
||||
result.current.add(T2);
|
||||
result.current.add(T3);
|
||||
result.current.update(T2.id, data);
|
||||
});
|
||||
expect(result.current.toasts[T2.id]).toHaveProperty(
|
||||
'content',
|
||||
data.content
|
||||
);
|
||||
expect(result.current.count).toEqual(3);
|
||||
});
|
||||
|
||||
it('removes all toasts', () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
act(() => {
|
||||
result.current.add(T1);
|
||||
result.current.add(T2);
|
||||
result.current.add(T3);
|
||||
result.current.removeAll();
|
||||
});
|
||||
expect(result.current.toasts).toEqual({});
|
||||
expect(result.current.count).toEqual(0);
|
||||
});
|
||||
|
||||
it('sends close signal to toast', () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
act(() => {
|
||||
result.current.add(T1);
|
||||
result.current.add(T2);
|
||||
result.current.add(T3);
|
||||
result.current.close(T2.id);
|
||||
});
|
||||
expect(result.current.toasts[T2.id]).toHaveProperty('signal', 'close');
|
||||
});
|
||||
|
||||
it('sends close signal to all toasts', () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
act(() => {
|
||||
result.current.add(T1);
|
||||
result.current.add(T2);
|
||||
result.current.add(T3);
|
||||
result.current.closeAll();
|
||||
});
|
||||
Object.values(result.current.toasts).forEach((t) => {
|
||||
expect(t).toHaveProperty('signal', 'close');
|
||||
});
|
||||
});
|
||||
|
||||
it('sets toast (adds or update if exists)', () => {
|
||||
const { result } = renderHook(() => useToasts());
|
||||
const data = { content: <p>Burning hot toast</p> };
|
||||
act(() => {
|
||||
result.current.setToast(T1);
|
||||
result.current.setToast(T1);
|
||||
result.current.setToast(T2);
|
||||
result.current.setToast(T3);
|
||||
result.current.setToast(T3);
|
||||
result.current.setToast({ ...T3, ...data });
|
||||
});
|
||||
expect(result.current.toasts[T3.id]).toHaveProperty(
|
||||
'content',
|
||||
data.content
|
||||
);
|
||||
expect(result.current.count).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,20 @@
|
||||
import { create } from 'zustand';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import type { Toast } from './toast';
|
||||
import omit from 'lodash/omit';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
type ToastsStore = {
|
||||
/**
|
||||
* A list of active toasts
|
||||
*/
|
||||
toasts: Toast[];
|
||||
export type Toasts = Record<string, Toast>;
|
||||
|
||||
const isUpdateable = (a: Toast, b: Toast) =>
|
||||
isEqual(omit(a, 'onClose'), omit(b, 'onClose'));
|
||||
|
||||
type State = {
|
||||
toasts: Toasts;
|
||||
count: number;
|
||||
};
|
||||
|
||||
type Actions = {
|
||||
/**
|
||||
* Adds/displays a new toast
|
||||
*/
|
||||
@@ -36,44 +45,54 @@ type ToastsStore = {
|
||||
removeAll: () => void;
|
||||
};
|
||||
|
||||
const add =
|
||||
(toast: Toast) =>
|
||||
(store: ToastsStore): Partial<ToastsStore> => ({
|
||||
toasts: [...store.toasts, toast],
|
||||
});
|
||||
type ToastsStore = State & Actions;
|
||||
|
||||
const update =
|
||||
(id: string, toastData: Partial<Toast>) =>
|
||||
(store: ToastsStore): Partial<ToastsStore> => {
|
||||
const toasts = [...store.toasts];
|
||||
const toastIdx = toasts.findIndex((t) => t.id === id);
|
||||
if (toastIdx > -1) toasts[toastIdx] = { ...toasts[toastIdx], ...toastData };
|
||||
return { toasts };
|
||||
};
|
||||
|
||||
export const useToasts = create<ToastsStore>((set) => ({
|
||||
toasts: [],
|
||||
add: (toast) => set(add(toast)),
|
||||
update: (id, toastData) => set(update(id, toastData)),
|
||||
setToast: (toast: Toast) =>
|
||||
set((store) => {
|
||||
if (store.toasts.find((t) => t.id === toast.id)) {
|
||||
return update(toast.id, toast)(store);
|
||||
} else {
|
||||
return add(toast)(store);
|
||||
}
|
||||
}),
|
||||
close: (id) => set(update(id, { signal: 'close' })),
|
||||
closeAll: () =>
|
||||
set((store) => ({
|
||||
toasts: [...store.toasts].map((t) => ({ ...t, signal: 'close' })),
|
||||
})),
|
||||
remove: (id) =>
|
||||
set((store) => ({
|
||||
toasts: [...store.toasts].filter((t) => t.id !== id),
|
||||
})),
|
||||
removeAll: () =>
|
||||
set(() => ({
|
||||
toasts: [],
|
||||
})),
|
||||
}));
|
||||
export const useToasts = create(
|
||||
immer<ToastsStore>((set, get) => ({
|
||||
toasts: {},
|
||||
count: 0,
|
||||
add: (toast) =>
|
||||
set((state) => {
|
||||
state.toasts[toast.id] = toast;
|
||||
++state.count;
|
||||
}),
|
||||
update: (id, toastData) =>
|
||||
set((state) => {
|
||||
const found = state.toasts[id];
|
||||
if (found) {
|
||||
Object.assign(found, toastData);
|
||||
}
|
||||
}),
|
||||
setToast: (toast: Toast) =>
|
||||
set((state) => {
|
||||
const found = state.toasts[toast.id];
|
||||
if (found) {
|
||||
if (!isUpdateable(found, toast)) {
|
||||
Object.assign(found, toast);
|
||||
}
|
||||
} else {
|
||||
state.toasts[toast.id] = toast;
|
||||
++state.count;
|
||||
}
|
||||
}),
|
||||
close: (id) =>
|
||||
set((state) => {
|
||||
const found = state.toasts[id];
|
||||
if (found) {
|
||||
found.signal = 'close';
|
||||
}
|
||||
}),
|
||||
closeAll: () =>
|
||||
set((state) => {
|
||||
Object.values(state.toasts).forEach((t) => (t.signal = 'close'));
|
||||
}),
|
||||
remove: (id) =>
|
||||
set((state) => {
|
||||
if (state.toasts[id]) {
|
||||
delete state.toasts[id];
|
||||
--state.count;
|
||||
}
|
||||
}),
|
||||
removeAll: () => set({ toasts: {}, count: 0 }),
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ export const getIntentBorder = (intent = Intent.None) => {
|
||||
|
||||
export const getIntentBackground = (intent?: Intent) => {
|
||||
return {
|
||||
'bg-neutral-200 dark:bg-neutral-800': intent === undefined,
|
||||
'bg-black dark:bg-white': intent === Intent.None,
|
||||
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
|
||||
'bg-danger': intent === Intent.Danger,
|
||||
|
||||
@@ -292,6 +292,40 @@ export interface BatchMarketInstructionSubmissionBody {
|
||||
};
|
||||
}
|
||||
|
||||
interface TransferBase {
|
||||
fromAccountType: Schema.AccountType;
|
||||
to: string;
|
||||
toAccountType: Schema.AccountType;
|
||||
asset: string;
|
||||
amount: string;
|
||||
reference?: string;
|
||||
}
|
||||
|
||||
export interface OneOffTransfer extends TransferBase {
|
||||
oneOff: {
|
||||
deliverOn?: number; // omit for immediate
|
||||
};
|
||||
}
|
||||
|
||||
export interface RecurringTransfer extends TransferBase {
|
||||
recurring: {
|
||||
factor: string;
|
||||
startEpoch: number;
|
||||
endEpoch?: number;
|
||||
dispatchStrategy?: {
|
||||
assetForMetric: string;
|
||||
metric: Schema.DispatchMetric;
|
||||
markets?: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type Transfer = OneOffTransfer | RecurringTransfer;
|
||||
|
||||
export interface TransferBody {
|
||||
transfer: Transfer;
|
||||
}
|
||||
|
||||
export type Transaction =
|
||||
| OrderSubmissionBody
|
||||
| OrderCancellationBody
|
||||
@@ -301,7 +335,8 @@ export type Transaction =
|
||||
| UndelegateSubmissionBody
|
||||
| OrderAmendmentBody
|
||||
| ProposalSubmissionBody
|
||||
| BatchMarketInstructionSubmissionBody;
|
||||
| BatchMarketInstructionSubmissionBody
|
||||
| TransferBody;
|
||||
|
||||
export const isWithdrawTransaction = (
|
||||
transaction: Transaction
|
||||
@@ -324,6 +359,10 @@ export const isBatchMarketInstructionsTransaction = (
|
||||
): transaction is BatchMarketInstructionSubmissionBody =>
|
||||
'batchMarketInstructions' in transaction;
|
||||
|
||||
export const isTransferTransaction = (
|
||||
transaction: Transaction
|
||||
): transaction is TransferBody => 'transfer' in transaction;
|
||||
|
||||
export interface TransactionResponse {
|
||||
transactionHash: string;
|
||||
signature: string; // still to be added by core
|
||||
|
||||
@@ -13,10 +13,12 @@ const mockSendTx = jest.fn<Promise<Partial<TransactionResponse> | null>, []>();
|
||||
|
||||
const pubKey = 'pubKey';
|
||||
|
||||
const mockDisconnect = jest.fn();
|
||||
jest.mock('./use-vega-wallet', () => ({
|
||||
useVegaWallet: () => ({
|
||||
sendTx: mockSendTx,
|
||||
pubKey,
|
||||
disconnect: mockDisconnect,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -104,6 +106,17 @@ describe('useVegaTransactionManager', () => {
|
||||
mockSendTx.mockRejectedValue(null);
|
||||
renderHook(useVegaTransactionManager);
|
||||
await waitForNextTick();
|
||||
expect(mockDisconnect).not.toHaveBeenCalledWith();
|
||||
expect(update).toBeCalled();
|
||||
expect(update.mock.calls[0][1]?.status).toEqual(VegaTxStatus.Error);
|
||||
});
|
||||
|
||||
it('call disconnect if detect no service error', async () => {
|
||||
mockTransactionStoreState.mockReturnValue(defaultState);
|
||||
mockSendTx.mockRejectedValue(new TypeError('Failed to fetch'));
|
||||
renderHook(useVegaTransactionManager);
|
||||
await waitForNextTick();
|
||||
expect(mockDisconnect).toHaveBeenCalledWith();
|
||||
expect(update).toBeCalled();
|
||||
expect(update.mock.calls[0][1]?.status).toEqual(VegaTxStatus.Error);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useVegaWallet } from './use-vega-wallet';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { WalletError } from './connectors';
|
||||
import { ClientErrors } from './connectors';
|
||||
import { VegaTxStatus } from './use-vega-transaction';
|
||||
import { VegaTxStatus, orderErrorResolve } from './use-vega-transaction';
|
||||
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
||||
import { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
|
||||
export const useVegaTransactionManager = () => {
|
||||
const { sendTx, pubKey } = useVegaWallet();
|
||||
const { sendTx, pubKey, disconnect } = useVegaWallet();
|
||||
const processed = useRef<Set<number>>(new Set());
|
||||
const transaction = useVegaTransactionStore((state) =>
|
||||
state.transactions.find(
|
||||
@@ -38,10 +38,14 @@ export const useVegaTransactionManager = () => {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = orderErrorResolve(err);
|
||||
if ((error as WalletError).code === ClientErrors.NO_SERVICE.code) {
|
||||
disconnect();
|
||||
}
|
||||
update(transaction.id, {
|
||||
error: err instanceof WalletClientError ? err : ClientErrors.UNKNOWN,
|
||||
error,
|
||||
status: VegaTxStatus.Error,
|
||||
});
|
||||
});
|
||||
}, [transaction, pubKey, del, sendTx, update]);
|
||||
}, [transaction, pubKey, del, sendTx, update, disconnect]);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isOrderCancellationTransaction,
|
||||
isOrderAmendmentTransaction,
|
||||
isBatchMarketInstructionsTransaction,
|
||||
isTransferTransaction,
|
||||
} from './connectors';
|
||||
import { determineId } from './utils';
|
||||
|
||||
@@ -19,6 +20,7 @@ import type {
|
||||
} from './__generated__/TransactionResult';
|
||||
|
||||
import type { WithdrawalApprovalQuery } from './__generated__/WithdrawalApproval';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
export interface VegaStoredTxState extends VegaTxState {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
@@ -50,8 +52,8 @@ export interface VegaTransactionStore {
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useVegaTransactionStore = create<VegaTransactionStore>(
|
||||
(set, get) => ({
|
||||
export const useVegaTransactionStore = create(
|
||||
subscribeWithSelector<VegaTransactionStore>((set, get) => ({
|
||||
transactions: [] as VegaStoredTxState[],
|
||||
create: (body: Transaction) => {
|
||||
const transactions = get().transactions;
|
||||
@@ -184,9 +186,14 @@ export const useVegaTransactionStore = create<VegaTransactionStore>(
|
||||
);
|
||||
if (transaction) {
|
||||
transaction.transactionResult = transactionResult;
|
||||
if (
|
||||
|
||||
const isConfirmedOrderCancellation =
|
||||
isOrderCancellationTransaction(transaction.body) &&
|
||||
!transaction.body.orderCancellation.orderId &&
|
||||
!transaction.body.orderCancellation.orderId;
|
||||
const isConfirmedTransfer = isTransferTransaction(transaction.body);
|
||||
|
||||
if (
|
||||
(isConfirmedOrderCancellation || isConfirmedTransfer) &&
|
||||
!transactionResult.error &&
|
||||
transactionResult.status
|
||||
) {
|
||||
@@ -198,5 +205,5 @@ export const useVegaTransactionStore = create<VegaTransactionStore>(
|
||||
})
|
||||
);
|
||||
},
|
||||
})
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -6,17 +6,18 @@ import {
|
||||
useTransactionEventSubscription,
|
||||
} from './__generated__/TransactionResult';
|
||||
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
||||
|
||||
import { waitForWithdrawalApproval } from './wait-for-withdrawal-approval';
|
||||
|
||||
export const useVegaTransactionUpdater = () => {
|
||||
const client = useApolloClient();
|
||||
const { updateWithdrawal, updateOrder, updateTransaction } =
|
||||
useVegaTransactionStore((state) => ({
|
||||
updateWithdrawal: state.updateWithdrawal,
|
||||
updateOrder: state.updateOrder,
|
||||
updateTransaction: state.updateTransactionResult,
|
||||
}));
|
||||
const updateWithdrawal = useVegaTransactionStore(
|
||||
(state) => state.updateWithdrawal
|
||||
);
|
||||
const updateOrder = useVegaTransactionStore((state) => state.updateOrder);
|
||||
const updateTransaction = useVegaTransactionStore(
|
||||
(state) => state.updateTransactionResult
|
||||
);
|
||||
|
||||
const { pubKey } = useVegaWallet();
|
||||
const variables = { partyId: pubKey || '' };
|
||||
const skip = !pubKey;
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('useVegaTransaction', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('handles an unkwown error', () => {
|
||||
it('handles an unknown error', () => {
|
||||
const unknownThrow = { foo: 'bar' };
|
||||
const mockSendTx = jest.fn(() => {
|
||||
throw unknownThrow;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
WalletClientError,
|
||||
WalletHttpError,
|
||||
} from '@vegaprotocol/wallet-client';
|
||||
import { useVegaWallet } from './use-vega-wallet';
|
||||
import type { VegaTransactionContentMap } from './vega-transaction-dialog';
|
||||
import { VegaTransactionDialog } from './vega-transaction-dialog';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transaction } from './connectors';
|
||||
import type { WalletError } from './connectors';
|
||||
import { ClientErrors } from './connectors';
|
||||
|
||||
export interface DialogProps {
|
||||
@@ -38,8 +43,21 @@ export const initialState = {
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
export const orderErrorResolve = (err: Error | unknown): Error => {
|
||||
if (err instanceof WalletClientError) {
|
||||
return err;
|
||||
} else if (err instanceof WalletHttpError) {
|
||||
return ClientErrors.UNKNOWN;
|
||||
} else if (err instanceof TypeError) {
|
||||
return ClientErrors.NO_SERVICE;
|
||||
} else if (err instanceof Error) {
|
||||
return err;
|
||||
}
|
||||
return ClientErrors.UNKNOWN;
|
||||
};
|
||||
|
||||
export const useVegaTransaction = () => {
|
||||
const { sendTx } = useVegaWallet();
|
||||
const { sendTx, disconnect } = useVegaWallet();
|
||||
const [transaction, _setTransaction] = useState<VegaTxState>(initialState);
|
||||
|
||||
const setTransaction = useCallback((update: Partial<VegaTxState>) => {
|
||||
@@ -88,7 +106,10 @@ export const useVegaTransaction = () => {
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : ClientErrors.UNKNOWN;
|
||||
const error = orderErrorResolve(err);
|
||||
if ((error as WalletError).code === ClientErrors.NO_SERVICE.code) {
|
||||
disconnect();
|
||||
}
|
||||
setTransaction({
|
||||
error,
|
||||
status: VegaTxStatus.Error,
|
||||
@@ -96,7 +117,7 @@ export const useVegaTransaction = () => {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[sendTx, setTransaction, reset]
|
||||
[sendTx, setTransaction, reset, disconnect]
|
||||
);
|
||||
|
||||
const Dialog = useMemo(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/react-helpers';
|
||||
import type { Market, Order } from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType, AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { sha3_256 } from 'js-sha3';
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
OrderAmendmentBody,
|
||||
OrderSubmissionBody,
|
||||
Transaction,
|
||||
Transfer,
|
||||
} from './connectors';
|
||||
|
||||
/**
|
||||
@@ -63,3 +64,23 @@ export const normalizeOrderAmendment = (
|
||||
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const normalizeTransfer = (
|
||||
address: string,
|
||||
amount: string,
|
||||
asset: {
|
||||
id: string;
|
||||
decimals: number;
|
||||
}
|
||||
): Transfer => {
|
||||
return {
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: address,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
// oneOff or recurring required otherwise wallet will error
|
||||
// default oneOff is immediate transfer
|
||||
oneOff: {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { DepositBusEventFieldsFragment } from '@vegaprotocol/wallet';
|
||||
|
||||
import type { EthTxState } from './use-ethereum-transaction';
|
||||
import { EthTxStatus } from './use-ethereum-transaction';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
type Contract = MultisigControl | CollateralBridge | Token | TokenFaucetable;
|
||||
type ContractMethod =
|
||||
@@ -54,8 +55,8 @@ export interface EthTransactionStore {
|
||||
delete: (index: number) => void;
|
||||
}
|
||||
|
||||
export const useEthTransactionStore = create<EthTransactionStore>(
|
||||
(set, get) => ({
|
||||
export const useEthTransactionStore = create(
|
||||
subscribeWithSelector<EthTransactionStore>((set, get) => ({
|
||||
transactions: [] as EthStoredTxState[],
|
||||
create: (
|
||||
contract: Contract | null,
|
||||
@@ -139,5 +140,5 @@ export const useEthTransactionStore = create<EthTransactionStore>(
|
||||
})
|
||||
);
|
||||
},
|
||||
})
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -103,7 +103,7 @@ export const useEthWithdrawApprovalsManager = () => {
|
||||
update(transaction.id, {
|
||||
status: ApprovalStatus.Ready,
|
||||
approval,
|
||||
dialogOpen: false,
|
||||
dialogOpen: true,
|
||||
});
|
||||
const signer = provider.getSigner();
|
||||
createEthTransaction(
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { WithdrawalBusEventFieldsFragment } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
|
||||
import type { WithdrawalApprovalQuery } from '@vegaprotocol/wallet';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
export enum ApprovalStatus {
|
||||
Idle = 'Idle',
|
||||
@@ -45,10 +46,11 @@ export interface EthWithdrawApprovalStore {
|
||||
>
|
||||
) => void;
|
||||
dismiss: (index: number) => void;
|
||||
delete: (index: number) => void;
|
||||
}
|
||||
|
||||
export const useEthWithdrawApprovalsStore = create<EthWithdrawApprovalStore>(
|
||||
(set, get) => ({
|
||||
export const useEthWithdrawApprovalsStore = create(
|
||||
subscribeWithSelector<EthWithdrawApprovalStore>((set, get) => ({
|
||||
transactions: [] as EthWithdrawalApprovalState[],
|
||||
create: (
|
||||
withdrawal: EthWithdrawalApprovalState['withdrawal'],
|
||||
@@ -107,5 +109,12 @@ export const useEthWithdrawApprovalsStore = create<EthWithdrawApprovalStore>(
|
||||
})
|
||||
);
|
||||
},
|
||||
})
|
||||
delete: (index: number) => {
|
||||
set(
|
||||
produce((state: EthWithdrawApprovalStore) => {
|
||||
delete state.transactions[index];
|
||||
})
|
||||
);
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -229,13 +229,15 @@ export const VerificationStatus = ({ state }: { state: VerifyState }) => {
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t("The amount you're withdrawing has triggered a time delay")}
|
||||
</p>
|
||||
<p>{t("The amount you're withdrawing has triggered a time delay")}</p>
|
||||
<p>{t(`Cannot be completed until ${formattedTime}`)}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === ApprovalStatus.Ready) {
|
||||
return <p>{t('The withdrawal has been approved.')}</p>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user