feat: deposit and deal ticket transaction stores and toasts (#2495)
This commit is contained in:
parent
4ce2924380
commit
5ccef2de5e
@ -1,5 +1,4 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { DealTicketManager } from '@vegaprotocol/deal-ticket';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
@ -50,10 +49,10 @@ export const DealTicketContainer = () => {
|
||||
);
|
||||
|
||||
const container = (
|
||||
<DealTicketManager market={data}>
|
||||
<>
|
||||
{loading ? loader : balance}
|
||||
<DealTicketSteps market={data} />
|
||||
</DealTicketManager>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { usePrevious } from './use-previous';
|
||||
import type { BigNumber } from '../lib/bignumber';
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
import colors from 'tailwindcss/colors';
|
||||
import { usePrevious } from '@vegaprotocol/react-helpers';
|
||||
const customColors = theme.colors;
|
||||
|
||||
const FLASH_DURATION = 1200; // Duration of flash animation in milliseconds
|
||||
|
@ -4,7 +4,7 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_CONFIG_URL=''
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_ENV=CUSTOM
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://stagnet1.console.vega.xyz\",\"STAGNET3\":\"https://stagnet3.console.vega.xyz\"}
|
||||
NX_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
NX_VEGA_URL=http://localhost:3028/query
|
||||
@ -13,7 +13,7 @@ NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
# Expose some env vars to cypress environment for market setup
|
||||
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
|
||||
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
|
||||
CYPRESS_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
|
||||
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY=02ecea…342f65
|
||||
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY2=7f9cf0…c25535
|
||||
|
@ -4,7 +4,7 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_CONFIG_URL=''
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_ENV=CUSTOM
|
||||
NX_VEGA_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_NETWORKS={\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://stagnet1.console.vega.xyz\",\"STAGNET3\":\"https://stagnet3.console.vega.xyz\"}
|
||||
NX_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
NX_VEGA_URL=http://localhost:3028/query
|
||||
@ -13,7 +13,7 @@ NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
# Expose some env vars to cypress environment for market setup
|
||||
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
|
||||
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
|
||||
CYPRESS_EXPLORER_URL=https://stagnet3.explorer.vega.xyz
|
||||
CYPRESS_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
|
||||
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY=02ecea…342f65
|
||||
CYPRESS_TRUNCATED_VEGA_PUBLIC_KEY2=7f9cf0…c25535
|
||||
|
@ -88,7 +88,7 @@ describe('orders list', { tags: '@smoke' }, () => {
|
||||
cy.get(`[row-id="${partiallyFilledId}"]`).within(() => {
|
||||
cy.get(`[col-id='${orderStatus}']`).should(
|
||||
'have.text',
|
||||
'PartiallyFilled'
|
||||
'Partially Filled'
|
||||
);
|
||||
cy.get(`[col-id='${orderRemaining}']`).should('have.text', '7/10');
|
||||
cy.getByTestId(cancelOrderBtn).should('not.exist');
|
||||
@ -190,7 +190,7 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should(
|
||||
'have.text',
|
||||
'PartiallyFilled'
|
||||
'Partially Filled'
|
||||
);
|
||||
cy.getByTestId(`order-status-${orderId}`)
|
||||
.parentsUntil(`.ag-row`)
|
||||
|
@ -21,6 +21,7 @@ export const testOrderSubmission = (
|
||||
orderSubmission: expectedOrder,
|
||||
};
|
||||
vegaWalletTransaction(transaction);
|
||||
verifyToast();
|
||||
};
|
||||
|
||||
export const testOrderAmendment = (
|
||||
@ -36,6 +37,7 @@ export const testOrderAmendment = (
|
||||
orderAmendment: expectedOrder,
|
||||
};
|
||||
vegaWalletTransaction(transaction);
|
||||
verifyToast();
|
||||
};
|
||||
|
||||
export const testOrderCancellation = (
|
||||
@ -51,11 +53,10 @@ export const testOrderCancellation = (
|
||||
orderCancellation: expectedOrder,
|
||||
};
|
||||
vegaWalletTransaction(transaction);
|
||||
verifyToast();
|
||||
};
|
||||
|
||||
const vegaWalletTransaction = (transaction: Transaction) => {
|
||||
const dialogTitle = 'dialog-title';
|
||||
const orderTransactionHash = 'tx-block-explorer';
|
||||
cy.wait('@VegaWalletTransaction')
|
||||
.its('request.body.params')
|
||||
.should('deep.equal', {
|
||||
@ -65,12 +66,13 @@ const vegaWalletTransaction = (transaction: Transaction) => {
|
||||
sendingMode: 'TYPE_SYNC',
|
||||
transaction,
|
||||
});
|
||||
cy.getByTestId(dialogTitle).should(
|
||||
'have.text',
|
||||
'Awaiting network confirmation'
|
||||
);
|
||||
cy.getByTestId(orderTransactionHash)
|
||||
.invoke('attr', 'href')
|
||||
.should('include', `${Cypress.env('EXPLORER_URL')}/txs/0xtest-tx-hash`);
|
||||
cy.getByTestId('dialog-close').click();
|
||||
};
|
||||
|
||||
const verifyToast = () => {
|
||||
cy.getByTestId('toast').should('contain.text', 'Awaiting confirmation');
|
||||
cy.getByTestId('toast')
|
||||
.find('a')
|
||||
.invoke('attr', 'href')
|
||||
.should('include', `${Cypress.env('EXPLORER_URL')}/txs/test-tx-hash`);
|
||||
cy.getByTestId('toast-close').click();
|
||||
};
|
||||
|
194
apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx
Normal file
194
apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
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 { ExternalLink, Intent, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
import {
|
||||
EthTxStatus,
|
||||
isEthereumError,
|
||||
TransactionContent,
|
||||
useEthTransactionStore,
|
||||
} from '@vegaprotocol/web3';
|
||||
|
||||
const intentMap: { [s in EthTxStatus]: Intent } = {
|
||||
Default: Intent.Primary,
|
||||
Requested: Intent.Warning,
|
||||
Pending: Intent.Warning,
|
||||
Error: Intent.Danger,
|
||||
Complete: Intent.Success,
|
||||
Confirmed: Intent.Success,
|
||||
};
|
||||
|
||||
const EthTransactionDetails = ({ tx }: { tx: EthStoredTxState }) => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
if (!data) return null;
|
||||
|
||||
const ETH_WITHDRAW =
|
||||
tx.methodName === 'withdraw_asset' && tx.args.length > 2 && tx.assetId;
|
||||
const ETH_DEPOSIT =
|
||||
tx.methodName === 'deposit_asset' && tx.args.length > 2 && tx.assetId;
|
||||
|
||||
let label = '';
|
||||
if (ETH_WITHDRAW) label = t('Withdraw');
|
||||
if (ETH_DEPOSIT) label = t('Deposit');
|
||||
|
||||
if (ETH_WITHDRAW || ETH_DEPOSIT) {
|
||||
const asset = data.find((a) => a.id === tx.assetId);
|
||||
|
||||
if (asset) {
|
||||
const num = formatNumber(
|
||||
toBigNum(tx.args[1], asset.decimals),
|
||||
asset.decimals
|
||||
);
|
||||
const details = (
|
||||
<div className="mt-[5px]">
|
||||
<span className="font-mono text-xs p-1 bg-gray-100 rounded">
|
||||
{label} {num} {asset.symbol}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{details}
|
||||
{tx.requiresConfirmation &&
|
||||
[EthTxStatus.Pending].includes(tx.status) && (
|
||||
<div className="mt-[10px]">
|
||||
<span className="font-mono text-xs">
|
||||
{t('Awaiting confirmations')}{' '}
|
||||
{`(${tx.confirmations}/${tx.requiredConfirmations})`}
|
||||
</span>
|
||||
<ProgressBar
|
||||
value={(tx.confirmations / tx.requiredConfirmations) * 100}
|
||||
intent={Intent.Warning}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type EthTxToastContentProps = {
|
||||
tx: EthStoredTxState;
|
||||
};
|
||||
|
||||
const EthTxRequestedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Action required')}</h3>
|
||||
<p>
|
||||
{t(
|
||||
'Please go to your wallet application and approve or reject the transaction.'
|
||||
)}
|
||||
</p>
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
|
||||
<p>{t('Please wait for your transaction to be confirmed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(ETHERSCAN_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EthTxErrorToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
let errorMessage = '';
|
||||
|
||||
if (isEthereumError(tx.error)) {
|
||||
errorMessage = tx.error.reason;
|
||||
} else if (tx.error instanceof Error) {
|
||||
errorMessage = tx.error.message;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Error occurred')}</h3>
|
||||
<p>{errorMessage}</p>
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Transaction completed')}</h3>
|
||||
<p>{t('Your transaction has been completed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(ETHERSCAN_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEthereumTransactionToasts = () => {
|
||||
const ethTransactions = useEthTransactionStore((state) =>
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
);
|
||||
const dismissEthTransaction = useEthTransactionStore(
|
||||
(state) => state.dismiss
|
||||
);
|
||||
const fromEthTransaction = useCallback(
|
||||
(tx: EthStoredTxState): Toast => {
|
||||
let content: ToastContent = <TransactionContent {...tx} />;
|
||||
if (tx.status === EthTxStatus.Requested) {
|
||||
content = <EthTxRequestedToastContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === EthTxStatus.Pending) {
|
||||
content = <EthTxPendingToastContent tx={tx} />;
|
||||
}
|
||||
if (
|
||||
tx.status === EthTxStatus.Confirmed ||
|
||||
tx.status === EthTxStatus.Complete
|
||||
) {
|
||||
content = <EthTxConfirmedToastContent tx={tx} />;
|
||||
}
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
content = <EthTxErrorToastContent tx={tx} />;
|
||||
}
|
||||
|
||||
return {
|
||||
id: `eth-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
onClose: () => dismissEthTransaction(tx.id),
|
||||
loader: tx.status === EthTxStatus.Pending,
|
||||
content,
|
||||
};
|
||||
},
|
||||
[dismissEthTransaction]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return [...compact(ethTransactions).map(fromEthTransaction)];
|
||||
}, [ethTransactions, fromEthTransaction]);
|
||||
};
|
@ -0,0 +1,78 @@
|
||||
import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { ApprovalStatus, VerificationStatus } from '@vegaprotocol/withdraws';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import type { EthWithdrawalApprovalState } from '@vegaprotocol/web3';
|
||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||
|
||||
const intentMap: { [s in ApprovalStatus]: Intent } = {
|
||||
Pending: Intent.Warning,
|
||||
Error: Intent.Danger,
|
||||
Idle: Intent.None,
|
||||
Delayed: Intent.Warning,
|
||||
Ready: Intent.Success,
|
||||
};
|
||||
|
||||
const EthWithdrawalApprovalToastContent = ({
|
||||
tx,
|
||||
}: {
|
||||
tx: EthWithdrawalApprovalState;
|
||||
}) => {
|
||||
let title = '';
|
||||
if (tx.status === ApprovalStatus.Error) {
|
||||
title = t('Error occurred');
|
||||
}
|
||||
if (tx.status === ApprovalStatus.Pending) {
|
||||
title = t('Pending approval');
|
||||
}
|
||||
if (tx.status === ApprovalStatus.Delayed) {
|
||||
title = t('Delayed');
|
||||
}
|
||||
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">
|
||||
{t('Withdraw')} {num} {tx.withdrawal.asset.symbol}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{title.length > 0 && <h3 className="font-bold">{title}</h3>}
|
||||
<VerificationStatus state={tx} />
|
||||
{details}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useEthereumWithdrawApprovalsToasts = () => {
|
||||
const { withdrawApprovals, dismissWithdrawApproval } =
|
||||
useEthWithdrawApprovalsStore((state) => ({
|
||||
withdrawApprovals: state.transactions.filter(
|
||||
(transaction) => transaction?.dialogOpen
|
||||
),
|
||||
dismissWithdrawApproval: state.dismiss,
|
||||
}));
|
||||
|
||||
const fromWithdrawalApproval = useCallback(
|
||||
(tx: EthWithdrawalApprovalState): Toast => ({
|
||||
id: `withdrawal-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
onClose: () => dismissWithdrawApproval(tx.id),
|
||||
loader: tx.status === ApprovalStatus.Pending,
|
||||
content: <EthWithdrawalApprovalToastContent tx={tx} />,
|
||||
}),
|
||||
[dismissWithdrawApproval]
|
||||
);
|
||||
|
||||
const toasts = useMemo(() => {
|
||||
return [...compact(withdrawApprovals).map(fromWithdrawalApproval)];
|
||||
}, [fromWithdrawalApproval, withdrawApprovals]);
|
||||
|
||||
return toasts;
|
||||
};
|
312
apps/trading/lib/hooks/use-vega-transaction-toasts.spec.tsx
Normal file
312
apps/trading/lib/hooks/use-vega-transaction-toasts.spec.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
OrderStatus,
|
||||
OrderTimeInForce,
|
||||
OrderType,
|
||||
Side,
|
||||
} from '@vegaprotocol/types';
|
||||
import type { VegaStoredTxState } from '@vegaprotocol/wallet';
|
||||
import { VegaTxStatus } from '@vegaprotocol/wallet';
|
||||
import { VegaTransactionDetails } from './use-vega-transaction-toasts';
|
||||
|
||||
jest.mock('@vegaprotocol/assets', () => {
|
||||
const A1 = {
|
||||
decimals: 2,
|
||||
id: 'asset-1',
|
||||
name: 'A1',
|
||||
quantum: '10',
|
||||
symbol: '$A',
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/assets'),
|
||||
useAssetsDataProvider: jest.fn(() => ({ data: [A1] })),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@vegaprotocol/market-list', () => {
|
||||
const M1 = {
|
||||
id: 'market-1',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
id: 'M1',
|
||||
name: 'M1',
|
||||
code: 'M1',
|
||||
product: {
|
||||
quoteName: '',
|
||||
settlementAsset: {
|
||||
id: 'asset-1',
|
||||
symbol: '$A',
|
||||
decimals: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/market-list'),
|
||||
useMarketList: jest.fn(() => ({ data: [M1] })),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@vegaprotocol/orders', () => {
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/orders'),
|
||||
useOrderByIdQuery: jest.fn(({ variables: { orderId } }) => {
|
||||
if (orderId === '0') {
|
||||
return {
|
||||
data: {
|
||||
orderByID: {
|
||||
id: '0',
|
||||
side: 'SIDE_BUY',
|
||||
size: '10',
|
||||
timeInForce: 'TIME_IN_FORCE_FOK',
|
||||
type: 'TYPE_MARKET',
|
||||
price: '1234',
|
||||
createdAt: new Date(),
|
||||
status: 'STATUS_ACTIVE',
|
||||
market: { id: 'market-1' },
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return { data: undefined };
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const unsupportedTransaction: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: { delegateSubmission: { nodeId: '1', amount: '0' } },
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const withdraw: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
withdrawSubmission: {
|
||||
amount: '1234',
|
||||
asset: 'asset-1',
|
||||
ext: { erc20: { receiverAddress: '0x0' } },
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const submitOrder: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
orderSubmission: {
|
||||
marketId: 'market-1',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '10',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
price: '1234',
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
order: {
|
||||
id: '0',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '10',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
price: '1234',
|
||||
createdAt: new Date(),
|
||||
market: {
|
||||
id: 'market-1',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'M1',
|
||||
name: 'M1',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
decimals: 2,
|
||||
symbol: '$A',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: OrderStatus.STATUS_ACTIVE,
|
||||
},
|
||||
};
|
||||
|
||||
const editOrder: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
orderAmendment: {
|
||||
marketId: 'market-1',
|
||||
orderId: '0',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
price: '1000',
|
||||
sizeDelta: 1,
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
order: {
|
||||
id: '0',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '10',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
price: '1234',
|
||||
createdAt: new Date(),
|
||||
market: {
|
||||
id: 'market-1',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'M1',
|
||||
name: 'M1',
|
||||
product: {
|
||||
settlementAsset: {
|
||||
decimals: 2,
|
||||
symbol: '$A',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: OrderStatus.STATUS_ACTIVE,
|
||||
},
|
||||
};
|
||||
|
||||
const cancelOrder: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
orderCancellation: {
|
||||
marketId: 'market-1',
|
||||
orderId: '0',
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const cancelAll: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
orderCancellation: {
|
||||
marketId: undefined,
|
||||
orderId: undefined,
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const closePosition: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
batchMarketInstructions: {
|
||||
cancellations: [{ marketId: 'market-1', orderId: '' }],
|
||||
submissions: [
|
||||
{
|
||||
marketId: 'market-1',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '10',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
price: '1234',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
const batch: VegaStoredTxState = {
|
||||
id: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
body: {
|
||||
batchMarketInstructions: {
|
||||
submissions: [
|
||||
{
|
||||
marketId: 'market-1',
|
||||
side: Side.SIDE_BUY,
|
||||
size: '10',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
type: OrderType.TYPE_MARKET,
|
||||
price: '1234',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
status: VegaTxStatus.Default,
|
||||
error: null,
|
||||
txHash: null,
|
||||
signature: null,
|
||||
dialogOpen: false,
|
||||
};
|
||||
|
||||
describe('VegaTransactionDetails', () => {
|
||||
it('does not display details if tx type cannot be determined', () => {
|
||||
const { queryByTestId } = render(
|
||||
<VegaTransactionDetails tx={unsupportedTransaction} />
|
||||
);
|
||||
expect(queryByTestId('vega-tx-details')).toBeNull();
|
||||
});
|
||||
it.each([
|
||||
{ tx: withdraw, details: 'Withdraw 12.34 $A' },
|
||||
{ tx: submitOrder, details: 'Submit order - activeM1+0.10 @ 12.34 $A' },
|
||||
{
|
||||
tx: editOrder,
|
||||
details: 'Edit order - activeM1+0.10 @ 12.34 $A+0.11 @ 10.00 $A',
|
||||
},
|
||||
{ tx: cancelOrder, details: 'Cancel orderM1+0.10 @ 12.34 $A' },
|
||||
{ tx: cancelAll, details: 'Cancel all orders' },
|
||||
{ tx: closePosition, details: 'Close position for M1' },
|
||||
{ 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);
|
||||
});
|
||||
});
|
567
apps/trading/lib/hooks/use-vega-transaction-toasts.tsx
Normal file
567
apps/trading/lib/hooks/use-vega-transaction-toasts.tsx
Normal file
@ -0,0 +1,567 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import type {
|
||||
BatchMarketInstructionSubmissionBody,
|
||||
OrderAmendment,
|
||||
OrderBusEventFieldsFragment,
|
||||
OrderCancellationBody,
|
||||
OrderSubmission,
|
||||
VegaStoredTxState,
|
||||
WithdrawalBusEventFieldsFragment,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { isBatchMarketInstructionsTransaction } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
ClientErrors,
|
||||
useReconnectVegaWallet,
|
||||
WalletError,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import {
|
||||
isOrderAmendmentTransaction,
|
||||
isOrderCancellationTransaction,
|
||||
isOrderSubmissionTransaction,
|
||||
isWithdrawTransaction,
|
||||
useVegaTransactionStore,
|
||||
VegaTxStatus,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
Size,
|
||||
t,
|
||||
toBigNum,
|
||||
} 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 { OrderStatusMapping } from '@vegaprotocol/types';
|
||||
|
||||
const intentMap: { [s in VegaTxStatus]: Intent } = {
|
||||
Default: Intent.Primary,
|
||||
Requested: Intent.Warning,
|
||||
Pending: Intent.Warning,
|
||||
Error: Intent.Danger,
|
||||
Complete: Intent.Success,
|
||||
};
|
||||
|
||||
const isClosePositionTransaction = (tx: VegaStoredTxState) => {
|
||||
if (isBatchMarketInstructionsTransaction(tx.body)) {
|
||||
const amendments =
|
||||
tx.body.batchMarketInstructions.amendments &&
|
||||
tx.body.batchMarketInstructions.amendments?.length > 0;
|
||||
|
||||
const cancellation =
|
||||
tx.body.batchMarketInstructions.cancellations?.length === 1 &&
|
||||
tx.body.batchMarketInstructions.cancellations[0].orderId === '' &&
|
||||
tx.body.batchMarketInstructions.cancellations[0];
|
||||
|
||||
const submission =
|
||||
cancellation &&
|
||||
tx.body.batchMarketInstructions.submissions?.length === 1 &&
|
||||
tx.body.batchMarketInstructions.submissions[0].marketId ===
|
||||
cancellation.marketId;
|
||||
|
||||
return !amendments && cancellation && submission;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
||||
const withdraw = isWithdrawTransaction(tx.body);
|
||||
const submitOrder = isOrderSubmissionTransaction(tx.body);
|
||||
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
||||
const editOrder = isOrderAmendmentTransaction(tx.body);
|
||||
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
|
||||
return (
|
||||
withdraw ||
|
||||
submitOrder ||
|
||||
cancelOrder ||
|
||||
editOrder ||
|
||||
batchMarketInstructions
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type SizeAtPriceProps = {
|
||||
side: Side;
|
||||
size: string;
|
||||
price: string | undefined;
|
||||
meta: { positionDecimalPlaces: number; decimalPlaces: number; asset: string };
|
||||
};
|
||||
const SizeAtPrice = ({ side, size, price, meta }: SizeAtPriceProps) => {
|
||||
return (
|
||||
<>
|
||||
<Size
|
||||
side={side}
|
||||
value={size}
|
||||
positionDecimalPlaces={meta.positionDecimalPlaces}
|
||||
forceTheme="light"
|
||||
/>{' '}
|
||||
{price && price !== '0' && meta.decimalPlaces
|
||||
? `@ ${addDecimalsFormatNumber(price, meta.decimalPlaces)} ${
|
||||
meta.asset
|
||||
}`
|
||||
: `@ ~ ${meta.asset}`}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SubmitOrderDetails = ({
|
||||
data,
|
||||
order,
|
||||
}: {
|
||||
data: OrderSubmission;
|
||||
order?: OrderBusEventFieldsFragment;
|
||||
}) => {
|
||||
const { data: markets } = useMarketList();
|
||||
const market = order
|
||||
? order.market
|
||||
: markets?.find((m) => m.id === data.marketId);
|
||||
if (!market) return null;
|
||||
|
||||
const price = order ? order.price : data.price;
|
||||
const size = order ? order.size : data.size;
|
||||
const side = order ? order.side : data.side;
|
||||
|
||||
return (
|
||||
<Details>
|
||||
<h4 className="font-bold">
|
||||
{order
|
||||
? t(
|
||||
`Submit order - ${OrderStatusMapping[order.status].toLowerCase()}`
|
||||
)
|
||||
: t('Submit order')}
|
||||
</h4>
|
||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
||||
<p>
|
||||
<SizeAtPrice
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.symbol,
|
||||
}}
|
||||
side={side}
|
||||
size={size}
|
||||
price={price}
|
||||
/>
|
||||
</p>
|
||||
{order && order.rejectionReason && (
|
||||
<p className="italic">{getRejectionReason(order)}</p>
|
||||
)}
|
||||
</Details>
|
||||
);
|
||||
};
|
||||
|
||||
const EditOrderDetails = ({
|
||||
data,
|
||||
order,
|
||||
}: {
|
||||
data: OrderAmendment;
|
||||
order?: OrderBusEventFieldsFragment;
|
||||
}) => {
|
||||
const { data: orderById } = useOrderByIdQuery({
|
||||
variables: { orderId: data.orderId },
|
||||
});
|
||||
const { data: markets } = useMarketList();
|
||||
|
||||
const originalOrder = orderById?.orderByID;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.find((m) => m.id === originalOrder.market.id);
|
||||
if (!market) return null;
|
||||
|
||||
const original = (
|
||||
<SizeAtPrice
|
||||
side={originalOrder.side}
|
||||
size={originalOrder.size}
|
||||
price={originalOrder.price}
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const edited = (
|
||||
<SizeAtPrice
|
||||
side={originalOrder.side}
|
||||
size={String(Number(originalOrder.size) + (data.sizeDelta || 0))}
|
||||
price={data.price}
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Details title={data.orderId}>
|
||||
<h4 className="font-bold">
|
||||
{order
|
||||
? t(`Edit order - ${OrderStatusMapping[order.status].toLowerCase()}`)
|
||||
: t('Edit order')}
|
||||
</h4>
|
||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
||||
<p>
|
||||
<s>{original}</s>
|
||||
</p>
|
||||
<p>{edited}</p>
|
||||
{order && order.rejectionReason && (
|
||||
<p className="italic">{getRejectionReason(order)}</p>
|
||||
)}
|
||||
</Details>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelOrderDetails = ({
|
||||
orderId,
|
||||
order,
|
||||
}: {
|
||||
orderId: string;
|
||||
order?: OrderBusEventFieldsFragment;
|
||||
}) => {
|
||||
const { data: orderById } = useOrderByIdQuery({
|
||||
variables: { orderId },
|
||||
});
|
||||
const { data: markets } = useMarketList();
|
||||
|
||||
const originalOrder = orderById?.orderByID;
|
||||
if (!originalOrder) return null;
|
||||
const market = markets?.find((m) => m.id === originalOrder.market.id);
|
||||
if (!market) return null;
|
||||
|
||||
const original = (
|
||||
<SizeAtPrice
|
||||
side={originalOrder.side}
|
||||
size={originalOrder.size}
|
||||
price={originalOrder.price}
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset:
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Details title={orderId}>
|
||||
<h4 className="font-bold">
|
||||
{order
|
||||
? t(
|
||||
`Cancel order - ${OrderStatusMapping[order.status].toLowerCase()}`
|
||||
)
|
||||
: t('Cancel order')}
|
||||
</h4>
|
||||
<p>{market?.tradableInstrument.instrument.code}</p>
|
||||
<p>
|
||||
<s>{original}</s>
|
||||
</p>
|
||||
{order && order.rejectionReason && (
|
||||
<p className="italic">{getRejectionReason(order)}</p>
|
||||
)}
|
||||
</Details>
|
||||
);
|
||||
};
|
||||
|
||||
export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
const { data: assets } = useAssetsDataProvider();
|
||||
const { data: markets } = useMarketList();
|
||||
|
||||
if (isWithdrawTransaction(tx.body)) {
|
||||
const transactionDetails = tx.body;
|
||||
const asset = assets?.find(
|
||||
(a) => a.id === transactionDetails.withdrawSubmission.asset
|
||||
);
|
||||
if (asset) {
|
||||
const num = formatNumber(
|
||||
toBigNum(transactionDetails.withdrawSubmission.amount, asset.decimals),
|
||||
asset.decimals
|
||||
);
|
||||
return (
|
||||
<Details>
|
||||
{t('Withdraw')} {num} {asset.symbol}
|
||||
</Details>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOrderSubmissionTransaction(tx.body)) {
|
||||
return (
|
||||
<SubmitOrderDetails data={tx.body.orderSubmission} order={tx.order} />
|
||||
);
|
||||
}
|
||||
|
||||
if (isOrderCancellationTransaction(tx.body)) {
|
||||
// CANCEL ALL (from Portfolio)
|
||||
if (
|
||||
tx.body.orderCancellation.marketId === undefined &&
|
||||
tx.body.orderCancellation.orderId === undefined
|
||||
) {
|
||||
return <Details>{t('Cancel all orders')}</Details>;
|
||||
}
|
||||
|
||||
// CANCEL
|
||||
if (
|
||||
tx.body.orderCancellation.orderId &&
|
||||
tx.body.orderCancellation.marketId
|
||||
) {
|
||||
return (
|
||||
<CancelOrderDetails
|
||||
orderId={String(tx.body.orderCancellation.orderId)}
|
||||
order={tx.order}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// CANCEL ALL (from Trading)
|
||||
if (tx.body.orderCancellation.marketId) {
|
||||
const marketName = markets?.find(
|
||||
(m) =>
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOrderAmendmentTransaction(tx.body)) {
|
||||
return (
|
||||
<EditOrderDetails
|
||||
data={tx.body.orderAmendment}
|
||||
order={tx.order}
|
||||
></EditOrderDetails>
|
||||
);
|
||||
}
|
||||
|
||||
if (isClosePositionTransaction(tx)) {
|
||||
const transaction = tx.body as BatchMarketInstructionSubmissionBody;
|
||||
const marketId = first(
|
||||
transaction.batchMarketInstructions.cancellations
|
||||
)?.marketId;
|
||||
const market = marketId && markets?.find((m) => m.id === marketId);
|
||||
if (market) {
|
||||
return (
|
||||
<Details>
|
||||
{t('Close position for')} {market.tradableInstrument.instrument.code}
|
||||
</Details>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isBatchMarketInstructionsTransaction(tx.body)) {
|
||||
return <Details>{t('Batch market instruction')}</Details>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type VegaTxToastContentProps = { tx: VegaStoredTxState };
|
||||
|
||||
const VegaTxRequestedToastContent = ({ tx }: VegaTxToastContentProps) => (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Action required')}</h3>
|
||||
<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>
|
||||
<p>{t('Please wait for your transaction to be confirmed')}</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} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
const { createEthWithdrawalApproval } = useEthWithdrawApprovalsStore(
|
||||
(state) => ({
|
||||
createEthWithdrawalApproval: state.create,
|
||||
})
|
||||
);
|
||||
const explorerLink = useLinks(DApp.Explorer);
|
||||
if (isWithdrawTransaction(tx.body)) {
|
||||
const completeWithdrawalButton = tx.withdrawal && (
|
||||
<div className="mt-[10px]">
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
createEthWithdrawalApproval(
|
||||
tx.withdrawal as WithdrawalBusEventFieldsFragment,
|
||||
tx.withdrawalApproval
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('Complete withdrawal')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Funds unlocked')}</h3>
|
||||
<p>{t('Your funds have been unlocked for withdrawal')}</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} />
|
||||
{completeWithdrawalButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Confirmed')}</h3>
|
||||
<p>{t('Your transaction has been confirmed ')}</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} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VegaTxErrorToastContent = ({ tx }: VegaTxToastContentProps) => {
|
||||
let label = t('Error occurred');
|
||||
let errorMessage = `${tx.error?.message} ${
|
||||
tx.error instanceof WalletError && tx.error?.data
|
||||
? `: ${tx.error?.data}`
|
||||
: ''
|
||||
}`;
|
||||
const reconnectVegaWallet = useReconnectVegaWallet();
|
||||
|
||||
const orderRejection = tx.order && getRejectionReason(tx.order);
|
||||
const walletNoConnectionCodes = [
|
||||
ClientErrors.NO_SERVICE.code,
|
||||
ClientErrors.NO_CLIENT.code,
|
||||
];
|
||||
const walletError =
|
||||
tx.error instanceof WalletError &&
|
||||
walletNoConnectionCodes.includes(tx.error.code);
|
||||
if (orderRejection) {
|
||||
label = t('Order rejected');
|
||||
errorMessage = orderRejection;
|
||||
}
|
||||
if (walletError) {
|
||||
label = t('Wallet disconnected');
|
||||
errorMessage = t('The connection to your Vega Wallet has been lost.');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{label}</h3>
|
||||
<p>{errorMessage}</p>
|
||||
{walletError && (
|
||||
<Button size="xs" onClick={reconnectVegaWallet}>
|
||||
{t('Connect vega wallet')}
|
||||
</Button>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useVegaTransactionToasts = () => {
|
||||
const vegaTransactions = useVegaTransactionStore((state) =>
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
);
|
||||
const dismissVegaTransaction = useVegaTransactionStore(
|
||||
(state) => state.dismiss
|
||||
);
|
||||
|
||||
const fromVegaTransaction = useCallback(
|
||||
(tx: VegaStoredTxState): Toast => {
|
||||
let content: ToastContent;
|
||||
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 {
|
||||
id: `vega-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
onClose: () => dismissVegaTransaction(tx.id),
|
||||
loader: tx.status === VegaTxStatus.Pending,
|
||||
content,
|
||||
};
|
||||
},
|
||||
[dismissVegaTransaction]
|
||||
);
|
||||
|
||||
const toasts = useMemo(() => {
|
||||
return [
|
||||
...compact(vegaTransactions)
|
||||
.filter((tx) => isTransactionTypeSupported(tx))
|
||||
.map(fromVegaTransaction),
|
||||
];
|
||||
}, [fromVegaTransaction, vegaTransactions]);
|
||||
|
||||
return toasts;
|
||||
};
|
@ -1,473 +1,32 @@
|
||||
import {
|
||||
Button,
|
||||
ExternalLink,
|
||||
Intent,
|
||||
ProgressBar,
|
||||
ToastsContainer,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useEthTransactionStore,
|
||||
useEthWithdrawApprovalsStore,
|
||||
TransactionContent,
|
||||
EthTxStatus,
|
||||
isEthereumError,
|
||||
ApprovalStatus,
|
||||
} from '@vegaprotocol/web3';
|
||||
import {
|
||||
isWithdrawTransaction,
|
||||
useVegaTransactionStore,
|
||||
VegaTxStatus,
|
||||
WalletError,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { VegaTransaction } from '../components/vega-transaction';
|
||||
import { VerificationStatus } from '@vegaprotocol/withdraws';
|
||||
import compact from 'lodash/compact';
|
||||
import { ToastsContainer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo } from 'react';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import type {
|
||||
EthStoredTxState,
|
||||
EthWithdrawalApprovalState,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import type {
|
||||
VegaStoredTxState,
|
||||
WithdrawSubmissionBody,
|
||||
WithdrawalBusEventFieldsFragment,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { formatNumber, t, toBigNum } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
DApp,
|
||||
ETHERSCAN_TX,
|
||||
EXPLORER_TX,
|
||||
useEtherscanLink,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/governance';
|
||||
|
||||
const intentMap = {
|
||||
Default: Intent.Primary,
|
||||
Requested: Intent.Warning,
|
||||
Pending: Intent.Warning,
|
||||
Error: Intent.Danger,
|
||||
Complete: Intent.Success,
|
||||
Confirmed: Intent.Success,
|
||||
Idle: Intent.None,
|
||||
Delayed: Intent.Warning,
|
||||
Ready: Intent.Success,
|
||||
};
|
||||
|
||||
const TransactionDetails = ({
|
||||
label,
|
||||
amount,
|
||||
asset,
|
||||
}: {
|
||||
label: string;
|
||||
amount: string;
|
||||
asset: Pick<Asset, 'symbol' | 'decimals'>;
|
||||
}) => {
|
||||
const num = formatNumber(toBigNum(amount, asset.decimals), asset.decimals);
|
||||
return (
|
||||
<div className="mt-[5px]">
|
||||
<span className="font-mono text-xs p-1 bg-gray-100 rounded">
|
||||
{label} {num} {asset.symbol}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
if (!data) return null;
|
||||
|
||||
const VEGA_WITHDRAW = isWithdrawTransaction(tx.body);
|
||||
if (VEGA_WITHDRAW) {
|
||||
const transactionDetails = tx.body as WithdrawSubmissionBody;
|
||||
const asset = data?.find(
|
||||
(a) => a.id === transactionDetails.withdrawSubmission.asset
|
||||
);
|
||||
if (asset) {
|
||||
return (
|
||||
<TransactionDetails
|
||||
label={t('Withdraw')}
|
||||
amount={transactionDetails.withdrawSubmission.amount}
|
||||
asset={asset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const EthTransactionDetails = ({ tx }: { tx: EthStoredTxState }) => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
if (!data) return null;
|
||||
|
||||
const ETH_WITHDRAW =
|
||||
tx.methodName === 'withdraw_asset' && tx.args.length > 2 && tx.asset;
|
||||
if (ETH_WITHDRAW) {
|
||||
const asset = data.find((a) => a.id === tx.asset);
|
||||
|
||||
if (asset) {
|
||||
return (
|
||||
<>
|
||||
<TransactionDetails
|
||||
label={t('Withdraw')}
|
||||
amount={tx.args[1]}
|
||||
asset={asset}
|
||||
/>
|
||||
{tx.requiresConfirmation && (
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
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 vegaTransactions = useVegaTransactionStore((state) =>
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
);
|
||||
const dismissVegaTransaction = useVegaTransactionStore(
|
||||
(state) => state.dismiss
|
||||
);
|
||||
const ethTransactions = useEthTransactionStore((state) =>
|
||||
state.transactions.filter((transaction) => transaction?.dialogOpen)
|
||||
);
|
||||
const dismissEthTransaction = useEthTransactionStore(
|
||||
(state) => state.dismiss
|
||||
);
|
||||
const { withdrawApprovals, createEthWithdrawalApproval } =
|
||||
useEthWithdrawApprovalsStore((state) => ({
|
||||
withdrawApprovals: state.transactions.filter(
|
||||
(transaction) => transaction?.dialogOpen
|
||||
),
|
||||
createEthWithdrawalApproval: state.create,
|
||||
}));
|
||||
const dismissWithdrawApproval = useEthWithdrawApprovalsStore(
|
||||
(state) => state.dismiss
|
||||
);
|
||||
const explorerLink = useLinks(DApp.Explorer);
|
||||
const etherscanLink = useEtherscanLink();
|
||||
|
||||
const fromVegaTransaction = useCallback(
|
||||
(tx: VegaStoredTxState): Toast => {
|
||||
let toast: Partial<Toast> = {};
|
||||
const defaultValues = {
|
||||
id: `vega-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
render: () => {
|
||||
return <VegaTransaction transaction={tx} />;
|
||||
},
|
||||
onClose: () => dismissVegaTransaction(tx.id),
|
||||
};
|
||||
if (tx.status === VegaTxStatus.Requested) {
|
||||
toast = {
|
||||
render: () => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Action required')}</h3>
|
||||
<p>
|
||||
{t(
|
||||
'Please go to your Vega wallet application and approve or reject the transaction.'
|
||||
)}
|
||||
</p>
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Pending) {
|
||||
toast = {
|
||||
render: () => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
|
||||
<p>{t('Please wait for your transaction to be confirmed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={explorerLink(
|
||||
EXPLORER_TX.replace(':hash', prepend0x(tx.txHash))
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View in block explorer')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
loader: true,
|
||||
};
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Complete) {
|
||||
toast = {
|
||||
render: () => {
|
||||
if (isWithdrawTransaction(tx.body)) {
|
||||
const completeWithdrawalButton = tx.withdrawal && (
|
||||
<div className="mt-[10px]">
|
||||
<Button
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
createEthWithdrawalApproval(
|
||||
tx.withdrawal as WithdrawalBusEventFieldsFragment,
|
||||
tx.withdrawalApproval
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('Complete withdrawal')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Funds unlocked')}</h3>
|
||||
<p>{t('Your funds have been unlocked for withdrawal')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={explorerLink(
|
||||
EXPLORER_TX.replace(':hash', prepend0x(tx.txHash))
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View in block explorer')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
{completeWithdrawalButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Confirmed')}</h3>
|
||||
<p>{t('Your transaction has been confirmed ')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={explorerLink(
|
||||
EXPLORER_TX.replace(':hash', prepend0x(tx.txHash))
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View in block explorer')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (tx.status === VegaTxStatus.Error) {
|
||||
toast = {
|
||||
render: () => {
|
||||
const error = `${tx.error?.message} ${
|
||||
tx.error instanceof WalletError
|
||||
? tx.error?.data
|
||||
? `: ${tx.error?.data}`
|
||||
: ''
|
||||
: ''
|
||||
}`;
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Error occurred')}</h3>
|
||||
<p>{error}</p>
|
||||
<VegaTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...defaultValues,
|
||||
...toast,
|
||||
};
|
||||
},
|
||||
[createEthWithdrawalApproval, dismissVegaTransaction, explorerLink]
|
||||
);
|
||||
|
||||
const fromEthTransaction = useCallback(
|
||||
(tx: EthStoredTxState): Toast => {
|
||||
let toast: Partial<Toast> = {};
|
||||
const defaultValues = {
|
||||
id: `eth-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
render: () => {
|
||||
return <TransactionContent {...tx} />;
|
||||
},
|
||||
onClose: () => dismissEthTransaction(tx.id),
|
||||
};
|
||||
if (tx.status === EthTxStatus.Requested) {
|
||||
toast = {
|
||||
render: () => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Action required')}</h3>
|
||||
<p>
|
||||
{t(
|
||||
'Please go to your wallet application and approve or reject the transaction.'
|
||||
)}
|
||||
</p>
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (tx.status === EthTxStatus.Pending) {
|
||||
toast = {
|
||||
render: () => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Awaiting confirmation')}</h3>
|
||||
<p>{t('Please wait for your transaction to be confirmed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(
|
||||
ETHERSCAN_TX.replace(':hash', tx.txHash)
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
loader: true,
|
||||
};
|
||||
}
|
||||
if (tx.status === EthTxStatus.Confirmed) {
|
||||
toast = {
|
||||
render: () => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Transaction completed')}</h3>
|
||||
<p>{t('Your transaction has been completed')}</p>
|
||||
{tx.txHash && (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(
|
||||
ETHERSCAN_TX.replace(':hash', tx.txHash)
|
||||
)}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
toast = {
|
||||
render: () => {
|
||||
let errorMessage = '';
|
||||
|
||||
if (isEthereumError(tx.error)) {
|
||||
errorMessage = tx.error.reason;
|
||||
} else if (tx.error instanceof Error) {
|
||||
errorMessage = tx.error.message;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h3 className="font-bold">{t('Error occurred')}</h3>
|
||||
<p>{errorMessage}</p>
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...defaultValues,
|
||||
...toast,
|
||||
};
|
||||
},
|
||||
[dismissEthTransaction, etherscanLink]
|
||||
);
|
||||
|
||||
const fromWithdrawalApproval = useCallback(
|
||||
(tx: EthWithdrawalApprovalState): Toast => ({
|
||||
id: `withdrawal-${tx.id}`,
|
||||
intent: intentMap[tx.status],
|
||||
render: () => {
|
||||
let title = '';
|
||||
if (tx.status === ApprovalStatus.Error) {
|
||||
title = t('Error occurred');
|
||||
}
|
||||
if (tx.status === ApprovalStatus.Pending) {
|
||||
title = t('Pending approval');
|
||||
}
|
||||
if (tx.status === ApprovalStatus.Delayed) {
|
||||
title = t('Delayed');
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{title.length > 0 && <h3 className="font-bold">{title}</h3>}
|
||||
<VerificationStatus state={tx} />
|
||||
<TransactionDetails
|
||||
label={t('Withdraw')}
|
||||
amount={tx.withdrawal.amount}
|
||||
asset={tx.withdrawal.asset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
onClose: () => dismissWithdrawApproval(tx.id),
|
||||
|
||||
loader: tx.status === ApprovalStatus.Pending,
|
||||
}),
|
||||
[dismissWithdrawApproval]
|
||||
);
|
||||
const vegaTransactionToasts = useVegaTransactionToasts();
|
||||
const ethTransactionToasts = useEthereumTransactionToasts();
|
||||
const withdrawApprovalToasts = useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
const toasts = useMemo(() => {
|
||||
return sortBy(
|
||||
[
|
||||
...compact(vegaTransactions).map(fromVegaTransaction),
|
||||
...compact(ethTransactions).map(fromEthTransaction),
|
||||
...compact(withdrawApprovals).map(fromWithdrawalApproval),
|
||||
...vegaTransactionToasts,
|
||||
...ethTransactionToasts,
|
||||
...withdrawApprovalToasts,
|
||||
...updateNetworkParametersToasts,
|
||||
],
|
||||
['createdBy']
|
||||
);
|
||||
}, [
|
||||
vegaTransactions,
|
||||
fromVegaTransaction,
|
||||
ethTransactions,
|
||||
fromEthTransaction,
|
||||
withdrawApprovals,
|
||||
fromWithdrawalApproval,
|
||||
vegaTransactionToasts,
|
||||
ethTransactionToasts,
|
||||
withdrawApprovalToasts,
|
||||
updateNetworkParametersToasts,
|
||||
]);
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { accountsDataProvider } from './accounts-data-provider';
|
||||
import type { Account } from './accounts-data-provider';
|
||||
import { getSettlementAccount } from './get-settlement-account';
|
||||
|
||||
export const useAccountBalance = (assetId: string) => {
|
||||
export const useAccountBalance = (assetId?: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [accountBalance, setAccountBalance] = useState<string>('');
|
||||
const [accountDecimals, setAccountDecimals] = useState<number | null>(null);
|
||||
@ -14,7 +14,9 @@ export const useAccountBalance = (assetId: string) => {
|
||||
}, [pubKey]);
|
||||
const update = useCallback(
|
||||
({ data }: { data: Account[] | null }) => {
|
||||
const account = getSettlementAccount({ accounts: data, assetId });
|
||||
const account = assetId
|
||||
? getSettlementAccount({ accounts: data, assetId })
|
||||
: undefined;
|
||||
if (accountBalance !== account?.balance) {
|
||||
setAccountBalance(account?.balance || '');
|
||||
}
|
||||
|
@ -4,31 +4,25 @@ import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
|
||||
interface Props {
|
||||
transactionStatus: 'default' | 'pending';
|
||||
disabled: boolean;
|
||||
variant: ButtonVariant;
|
||||
}
|
||||
|
||||
export const DealTicketButton = ({
|
||||
transactionStatus,
|
||||
disabled,
|
||||
variant,
|
||||
}: Props) => {
|
||||
export const DealTicketButton = ({ disabled, variant }: Props) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
const isPending = transactionStatus === 'pending';
|
||||
return pubKey ? (
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
variant={variant}
|
||||
fill
|
||||
type="submit"
|
||||
disabled={disabled || isPending}
|
||||
disabled={disabled}
|
||||
data-testid="place-order"
|
||||
>
|
||||
{isPending ? t('Pending...') : t('Place order')}
|
||||
{t('Place order')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -5,8 +5,9 @@ import type {
|
||||
MarketDataUpdateFieldsFragment,
|
||||
MarketDealTicket,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
import { marketDealTicketProvider } from '@vegaprotocol/market-list';
|
||||
import { DealTicketManager } from './deal-ticket-manager';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
|
||||
export interface DealTicketContainerProps {
|
||||
marketId: string;
|
||||
@ -27,6 +28,7 @@ export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => {
|
||||
variables,
|
||||
skip: !marketId,
|
||||
});
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
|
||||
return (
|
||||
<AsyncRenderer<MarketDealTicket>
|
||||
@ -35,7 +37,10 @@ export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => {
|
||||
error={error}
|
||||
>
|
||||
{data ? (
|
||||
<DealTicketManager market={data} />
|
||||
<DealTicket
|
||||
market={data}
|
||||
submit={(orderSubmission) => create({ orderSubmission })}
|
||||
/>
|
||||
) : (
|
||||
<Splash>
|
||||
<p>{t('Could not load market')}</p>
|
||||
|
@ -1,171 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
VegaTxStatus,
|
||||
WalletError,
|
||||
useVegaWallet,
|
||||
useVegaWalletDialogStore,
|
||||
ClientErrors,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
import type { MarketDealTicket } from '@vegaprotocol/market-list';
|
||||
import { useOrderSubmit, OrderFeedback } from '@vegaprotocol/orders';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { Button, Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export interface DealTicketManagerProps {
|
||||
market: MarketDealTicket;
|
||||
children?: ReactNode | ReactNode[];
|
||||
}
|
||||
|
||||
interface ErrorContentProps {
|
||||
transaction: VegaTxState;
|
||||
reset: () => void;
|
||||
}
|
||||
const ErrorContent = ({ transaction, reset }: ErrorContentProps) => {
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
const { disconnect } = useVegaWallet();
|
||||
const reconnect = useCallback(async () => {
|
||||
reset();
|
||||
await disconnect();
|
||||
openVegaWalletDialog();
|
||||
}, [reset, disconnect, openVegaWalletDialog]);
|
||||
return useMemo(() => {
|
||||
const { error } = transaction;
|
||||
if (error) {
|
||||
if (
|
||||
error instanceof WalletError &&
|
||||
error.code === ClientErrors.NO_SERVICE.code
|
||||
) {
|
||||
return (
|
||||
<ul data-testid="connectors-list" className="mb-6">
|
||||
<li className="mb-2 last:mb-0" data-testid={transaction.status}>
|
||||
{t('The connection to your Vega Wallet has been lost.')}
|
||||
</li>
|
||||
<li className="mb-0 pt-2">
|
||||
<Button onClick={reconnect}>{t('Connect vega wallet')}</Button>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p data-testid={transaction.status}>
|
||||
{error.message}{' '}
|
||||
{error instanceof WalletError ? `: ${error.data}` : null}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [transaction, reconnect]);
|
||||
};
|
||||
|
||||
export const DealTicketManager = ({
|
||||
market,
|
||||
children,
|
||||
}: DealTicketManagerProps) => {
|
||||
const { submit, transaction, finalizedOrder, Dialog, reset } =
|
||||
useOrderSubmit();
|
||||
return (
|
||||
<>
|
||||
{children || (
|
||||
<DealTicket
|
||||
market={market}
|
||||
submit={(order) => submit(order)}
|
||||
transactionStatus={
|
||||
transaction.status === VegaTxStatus.Requested ||
|
||||
transaction.status === VegaTxStatus.Pending
|
||||
? 'pending'
|
||||
: 'default'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Dialog
|
||||
title={getOrderDialogTitle(finalizedOrder?.status)}
|
||||
intent={getOrderDialogIntent(finalizedOrder?.status)}
|
||||
icon={getOrderDialogIcon(finalizedOrder?.status)}
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback transaction={transaction} order={finalizedOrder} />
|
||||
),
|
||||
Error: <ErrorContent transaction={transaction} reset={reset} />,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getOrderDialogTitle = (
|
||||
status?: Schema.OrderStatus
|
||||
): string | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||
return t('Order submitted');
|
||||
case Schema.OrderStatus.STATUS_FILLED:
|
||||
return t('Order filled');
|
||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||
return t('Order partially filled');
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
return t('Order parked');
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
return t('Order stopped');
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return t('Order cancelled');
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
return t('Order expired');
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
return t('Order rejected');
|
||||
default:
|
||||
return t('Submission failed');
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderDialogIntent = (
|
||||
status?: Schema.OrderStatus
|
||||
): Intent | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
case Schema.OrderStatus.STATUS_PARTIALLY_FILLED:
|
||||
return Intent.Warning;
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return Intent.Danger;
|
||||
case Schema.OrderStatus.STATUS_FILLED:
|
||||
case Schema.OrderStatus.STATUS_ACTIVE:
|
||||
return Intent.Success;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const getOrderDialogIcon = (
|
||||
status?: Schema.OrderStatus
|
||||
): ReactNode | undefined => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case Schema.OrderStatus.STATUS_PARKED:
|
||||
case Schema.OrderStatus.STATUS_EXPIRED:
|
||||
return <Icon name="warning-sign" />;
|
||||
case Schema.OrderStatus.STATUS_REJECTED:
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
case Schema.OrderStatus.STATUS_CANCELLED:
|
||||
return <Icon name="error" />;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
@ -26,7 +26,6 @@ jest.mock('../../hooks/use-has-no-balance', () => {
|
||||
|
||||
const market = generateMarket();
|
||||
const submit = jest.fn();
|
||||
const transactionStatus = 'default';
|
||||
|
||||
const mockChainId = 'chain-id';
|
||||
|
||||
@ -46,12 +45,7 @@ function generateJsx(order?: OrderSubmissionBody['orderSubmission']) {
|
||||
return (
|
||||
<MockedProvider mocks={[chainIdMock]}>
|
||||
<VegaWalletContext.Provider value={{ pubKey: mockChainId } as any}>
|
||||
<DealTicket
|
||||
defaultOrder={order}
|
||||
market={market}
|
||||
submit={submit}
|
||||
transactionStatus={transactionStatus}
|
||||
/>
|
||||
<DealTicket market={market} submit={submit} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { removeDecimal, t } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
@ -10,6 +10,7 @@ import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
|
||||
@ -32,8 +33,6 @@ export type TransactionStatus = 'default' | 'pending';
|
||||
export interface DealTicketProps {
|
||||
market: MarketDealTicket;
|
||||
submit: (order: OrderSubmissionBody['orderSubmission']) => void;
|
||||
transactionStatus: TransactionStatus;
|
||||
defaultOrder?: OrderSubmissionBody['orderSubmission'];
|
||||
}
|
||||
|
||||
export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
|
||||
@ -42,11 +41,7 @@ export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export const DealTicket = ({
|
||||
market,
|
||||
submit,
|
||||
transactionStatus,
|
||||
}: DealTicketProps) => {
|
||||
export const DealTicket = ({ market, submit }: DealTicketProps) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [persistedOrder, setPersistedOrder] = usePersistedOrder(market);
|
||||
const {
|
||||
@ -123,15 +118,13 @@ export const DealTicket = ({
|
||||
return;
|
||||
}
|
||||
|
||||
submit({
|
||||
...order,
|
||||
price: order.price && removeDecimal(order.price, market.decimalPlaces),
|
||||
size: removeDecimal(order.size, market.positionDecimalPlaces),
|
||||
expiresAt:
|
||||
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
? order.expiresAt
|
||||
: undefined,
|
||||
});
|
||||
submit(
|
||||
normalizeOrderSubmission(
|
||||
order,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
);
|
||||
},
|
||||
[
|
||||
submit,
|
||||
@ -209,7 +202,6 @@ export const DealTicket = ({
|
||||
)}
|
||||
<DealTicketButton
|
||||
disabled={Object.keys(errors).length >= 1}
|
||||
transactionStatus={transactionStatus}
|
||||
variant={order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'}
|
||||
/>
|
||||
<SummaryMessage
|
||||
|
@ -1,7 +1,6 @@
|
||||
export * from './deal-ticket-amount';
|
||||
export * from './deal-ticket-container';
|
||||
export * from './deal-ticket-limit-amount';
|
||||
export * from './deal-ticket-manager';
|
||||
export * from './deal-ticket-market-amount';
|
||||
export * from './deal-ticket';
|
||||
export * from './expiry-selector';
|
||||
|
@ -5,6 +5,7 @@ import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DepositContainer } from './deposit-container';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
@ -41,6 +42,9 @@ const DEFAULT_STYLE: DepositDialogStyleProps = {
|
||||
|
||||
export const DepositDialog = () => {
|
||||
const { assetId, isOpen, open, close } = useDepositDialog();
|
||||
const assetDetailsDialogOpen = useAssetDetailsDialogStore(
|
||||
(state) => state.isOpen
|
||||
);
|
||||
const connectWalletDialogIsOpen = useWeb3ConnectStore(
|
||||
(state) => state.isOpen
|
||||
);
|
||||
@ -55,7 +59,7 @@ export const DepositDialog = () => {
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen && !connectWalletDialogIsOpen}
|
||||
open={isOpen && !(connectWalletDialogIsOpen || assetDetailsDialogOpen)}
|
||||
onChange={(isOpen) => (isOpen ? open() : close())}
|
||||
{...dialogStyleProps}
|
||||
>
|
||||
|
@ -322,7 +322,6 @@ const FormButton = ({
|
||||
</Button>
|
||||
);
|
||||
} else if (chainId !== desiredChainId) {
|
||||
console.log(chainId, desiredChainId);
|
||||
const chainName = getChainName(desiredChainId);
|
||||
message = t(`This app only works on ${chainName}.`);
|
||||
button = (
|
||||
|
@ -1,16 +1,23 @@
|
||||
import { DepositForm } from './deposit-form';
|
||||
import { useSubmitDeposit } from './use-submit-deposit';
|
||||
import type { DepositFormProps } from './deposit-form';
|
||||
import { removeDecimal } from '@vegaprotocol/react-helpers';
|
||||
import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { useSubmitApproval } from './use-submit-approval';
|
||||
import { useSubmitFaucet } from './use-submit-faucet';
|
||||
import { useDepositStore } from './deposit-store';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDepositBalances } from './use-deposit-balances';
|
||||
import { useDepositDialog } from './deposit-dialog';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
|
||||
import pick from 'lodash/pick';
|
||||
import type { EthTransaction } from '@vegaprotocol/web3';
|
||||
import { EthTxStatus } from '@vegaprotocol/web3';
|
||||
import {
|
||||
EthTxStatus,
|
||||
useEthTransactionStore,
|
||||
useBridgeContract,
|
||||
useEthereumConfig,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface DepositManagerProps {
|
||||
@ -20,53 +27,36 @@ interface DepositManagerProps {
|
||||
setDialogStyleProps?: DepositDialogStylePropsSetter;
|
||||
}
|
||||
|
||||
const useDepositAsset = (assets: Asset[], assetId?: string) => {
|
||||
const { asset, balance, allowance, deposited, max, update } =
|
||||
useDepositStore();
|
||||
|
||||
const handleSelectAsset = useCallback(
|
||||
(id: string) => {
|
||||
const asset = assets.find((a) => a.id === id);
|
||||
update({ asset });
|
||||
},
|
||||
[assets, update]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleSelectAsset(assetId || '');
|
||||
}, [assetId, handleSelectAsset]);
|
||||
|
||||
return { asset, balance, allowance, deposited, max, handleSelectAsset };
|
||||
};
|
||||
|
||||
const getProps = (txContent?: EthTransaction['TxContent']) =>
|
||||
txContent ? pick(txContent, ['title', 'icon', 'intent']) : undefined;
|
||||
|
||||
export const DepositManager = ({
|
||||
assetId,
|
||||
assetId: initialAssetId,
|
||||
assets,
|
||||
isFaucetable,
|
||||
setDialogStyleProps,
|
||||
}: DepositManagerProps) => {
|
||||
const { asset, balance, allowance, deposited, max, handleSelectAsset } =
|
||||
useDepositAsset(assets, assetId);
|
||||
const createEthTransaction = useEthTransactionStore((state) => state.create);
|
||||
const { config } = useEthereumConfig();
|
||||
const [assetId, setAssetId] = useState(initialAssetId);
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
const bridgeContract = useBridgeContract();
|
||||
const closeDepositDialog = useDepositDialog((state) => state.close);
|
||||
|
||||
useDepositBalances(isFaucetable);
|
||||
const { balance, allowance, deposited, max, refresh } = useDepositBalances(
|
||||
asset,
|
||||
isFaucetable
|
||||
);
|
||||
|
||||
// Set up approve transaction
|
||||
const approve = useSubmitApproval();
|
||||
|
||||
// Set up deposit transaction
|
||||
const deposit = useSubmitDeposit();
|
||||
const approve = useSubmitApproval(asset);
|
||||
|
||||
// Set up faucet transaction
|
||||
const faucet = useSubmitFaucet();
|
||||
const faucet = useSubmitFaucet(asset);
|
||||
|
||||
const transactionInProgress = [
|
||||
approve.TxContent,
|
||||
deposit.TxContent,
|
||||
faucet.TxContent,
|
||||
].filter((t) => t.status !== EthTxStatus.Default)[0];
|
||||
const transactionInProgress = [approve.TxContent, faucet.TxContent].filter(
|
||||
(t) => t.status !== EthTxStatus.Default
|
||||
)[0];
|
||||
|
||||
useEffect(() => {
|
||||
setDialogStyleProps?.(getProps(transactionInProgress));
|
||||
@ -74,17 +64,44 @@ export const DepositManager = ({
|
||||
|
||||
const returnLabel = t('Return to deposit');
|
||||
|
||||
const submitDeposit = (
|
||||
args: Parameters<DepositFormProps['submitDeposit']>['0']
|
||||
) => {
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
createEthTransaction(
|
||||
bridgeContract,
|
||||
'deposit_asset',
|
||||
[
|
||||
args.assetSource,
|
||||
removeDecimal(args.amount, asset.decimals),
|
||||
prepend0x(args.vegaPublicKey),
|
||||
],
|
||||
asset.id,
|
||||
config?.confirmations ?? 1,
|
||||
true
|
||||
);
|
||||
closeDepositDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!transactionInProgress && (
|
||||
<DepositForm
|
||||
balance={balance}
|
||||
selectedAsset={asset}
|
||||
onSelectAsset={handleSelectAsset}
|
||||
onSelectAsset={setAssetId}
|
||||
assets={sortBy(assets, 'name')}
|
||||
submitApprove={() => approve.perform()}
|
||||
submitDeposit={(args) => deposit.perform(args)}
|
||||
requestFaucet={() => faucet.perform()}
|
||||
submitApprove={async () => {
|
||||
await approve.perform();
|
||||
refresh();
|
||||
}}
|
||||
submitDeposit={submitDeposit}
|
||||
requestFaucet={async () => {
|
||||
await faucet.perform();
|
||||
refresh();
|
||||
}}
|
||||
deposited={deposited}
|
||||
max={max}
|
||||
allowance={allowance}
|
||||
@ -94,7 +111,6 @@ export const DepositManager = ({
|
||||
|
||||
<approve.TxContent.Content returnLabel={returnLabel} />
|
||||
<faucet.TxContent.Content returnLabel={returnLabel} />
|
||||
<deposit.TxContent.Content returnLabel={returnLabel} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +0,0 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import create from 'zustand';
|
||||
|
||||
interface DepositStore {
|
||||
balance: BigNumber;
|
||||
allowance: BigNumber;
|
||||
asset: Asset | undefined;
|
||||
deposited: BigNumber;
|
||||
max: BigNumber;
|
||||
update: (state: Partial<DepositStore>) => void;
|
||||
}
|
||||
|
||||
export const useDepositStore = create<DepositStore>((set) => ({
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
deposited: new BigNumber(0),
|
||||
max: new BigNumber(0),
|
||||
asset: undefined,
|
||||
update: (updatedState) => {
|
||||
set(updatedState);
|
||||
},
|
||||
}));
|
@ -3,7 +3,6 @@ export * from './deposit-container';
|
||||
export * from './deposit-form';
|
||||
export * from './deposit-limits';
|
||||
export * from './deposit-manager';
|
||||
export * from './deposit-store';
|
||||
export * from './deposits-table';
|
||||
export * from './use-deposit-balances';
|
||||
export * from './use-deposits';
|
||||
@ -12,6 +11,5 @@ export * from './use-get-balance-of-erc20-token';
|
||||
export * from './use-get-deposit-maximum';
|
||||
export * from './use-get-deposited-amount';
|
||||
export * from './use-submit-approval';
|
||||
export * from './use-submit-deposit';
|
||||
export * from './use-submit-faucet';
|
||||
export * from './deposit-dialog';
|
||||
|
@ -1,19 +1,40 @@
|
||||
import { useBridgeContract, useTokenContract } from '@vegaprotocol/web3';
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useDepositStore } from './deposit-store';
|
||||
import { useGetAllowance } from './use-get-allowance';
|
||||
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
|
||||
import { useGetDepositMaximum } from './use-get-deposit-maximum';
|
||||
import { useGetDepositedAmount } from './use-get-deposited-amount';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
|
||||
import { isAssetTypeERC20, usePrevious } from '@vegaprotocol/react-helpers';
|
||||
import { useAccountBalance } from '@vegaprotocol/accounts';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
|
||||
type DepositBalances = {
|
||||
balance: BigNumber;
|
||||
allowance: BigNumber;
|
||||
deposited: BigNumber;
|
||||
max: BigNumber;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
type DepositBalancesState = Omit<DepositBalances, 'refresh'>;
|
||||
|
||||
const initialState: DepositBalancesState = {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
deposited: new BigNumber(0),
|
||||
max: new BigNumber(0),
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook which fetches all the balances required for depositing
|
||||
* whenever the asset changes in the form
|
||||
*/
|
||||
export const useDepositBalances = (isFaucetable: boolean) => {
|
||||
const { asset, update } = useDepositStore();
|
||||
export const useDepositBalances = (
|
||||
asset: Asset | undefined,
|
||||
isFaucetable: boolean
|
||||
): DepositBalances => {
|
||||
const tokenContract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
isFaucetable
|
||||
@ -23,37 +44,42 @@ export const useDepositBalances = (isFaucetable: boolean) => {
|
||||
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
|
||||
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
|
||||
const getDepositedAmount = useGetDepositedAmount(asset);
|
||||
const prevAsset = usePrevious(asset);
|
||||
const [state, setState] = useState<DepositBalancesState>(initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const getBalances = async () => {
|
||||
try {
|
||||
const [max, deposited, balance, allowance] = await Promise.all([
|
||||
getDepositMaximum(),
|
||||
getDepositedAmount(),
|
||||
getBalance(),
|
||||
getAllowance(),
|
||||
]);
|
||||
|
||||
update({
|
||||
max,
|
||||
deposited,
|
||||
balance,
|
||||
allowance,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (asset) {
|
||||
getBalances();
|
||||
if (asset?.id !== prevAsset?.id) {
|
||||
// reset values to initial state when asset changes
|
||||
setState(initialState);
|
||||
}
|
||||
}, [
|
||||
asset,
|
||||
update,
|
||||
getDepositMaximum,
|
||||
getDepositedAmount,
|
||||
getAllowance,
|
||||
getBalance,
|
||||
]);
|
||||
}, [asset?.id, prevAsset?.id]);
|
||||
|
||||
const { accountBalance } = useAccountBalance(asset?.id);
|
||||
|
||||
const getBalances = useCallback(async () => {
|
||||
if (!asset) return;
|
||||
try {
|
||||
const [max, deposited, balance, allowance] = await Promise.all([
|
||||
getDepositMaximum(),
|
||||
getDepositedAmount(),
|
||||
getBalance(),
|
||||
getAllowance(),
|
||||
]);
|
||||
|
||||
setState({
|
||||
max: max ?? initialState.max,
|
||||
deposited: deposited ?? initialState.deposited,
|
||||
balance: balance ?? initialState.balance,
|
||||
allowance: allowance ?? initialState.allowance,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]);
|
||||
|
||||
useEffect(() => {
|
||||
getBalances();
|
||||
}, [asset, getBalances, accountBalance]);
|
||||
|
||||
return { ...state, refresh: getBalances };
|
||||
};
|
||||
|
@ -6,17 +6,14 @@ import {
|
||||
useEthereumTransaction,
|
||||
useTokenContract,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { useDepositStore } from './deposit-store';
|
||||
import { useGetAllowance } from './use-get-allowance';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
|
||||
export const useSubmitApproval = () => {
|
||||
export const useSubmitApproval = (asset?: Asset) => {
|
||||
const { config } = useEthereumConfig();
|
||||
const { asset, update } = useDepositStore();
|
||||
const contract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
true
|
||||
);
|
||||
const getAllowance = useGetAllowance(contract, asset);
|
||||
const transaction = useEthereumTransaction<Token, 'approve'>(
|
||||
contract,
|
||||
'approve'
|
||||
@ -31,8 +28,6 @@ export const useSubmitApproval = () => {
|
||||
config.collateral_bridge_contract.address,
|
||||
amount
|
||||
);
|
||||
const allowance = await getAllowance();
|
||||
update({ allowance });
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
|
@ -1,103 +0,0 @@
|
||||
import { useSubscription } from '@apollo/client';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import type {
|
||||
DepositEventSubscription,
|
||||
DepositEventSubscriptionVariables,
|
||||
} from './__generated__/Deposit';
|
||||
import { DepositEventDocument } from './__generated__/Deposit';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
isAssetTypeERC20,
|
||||
remove0x,
|
||||
removeDecimal,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useBridgeContract,
|
||||
useEthereumConfig,
|
||||
useEthereumTransaction,
|
||||
useTokenContract,
|
||||
} from '@vegaprotocol/web3';
|
||||
import type { CollateralBridge } from '@vegaprotocol/smart-contracts';
|
||||
import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import { useDepositStore } from './deposit-store';
|
||||
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
|
||||
|
||||
export const useSubmitDeposit = () => {
|
||||
const { asset, update } = useDepositStore();
|
||||
const { config } = useEthereumConfig();
|
||||
const bridgeContract = useBridgeContract();
|
||||
const tokenContract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
true
|
||||
);
|
||||
|
||||
// Store public key from contract arguments for use in the subscription,
|
||||
// NOTE: it may be different from the users connected key
|
||||
const [partyId, setPartyId] = useState<string | null>(null);
|
||||
|
||||
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
|
||||
|
||||
const transaction = useEthereumTransaction<CollateralBridge, 'deposit_asset'>(
|
||||
bridgeContract,
|
||||
'deposit_asset',
|
||||
config?.confirmations,
|
||||
true
|
||||
);
|
||||
|
||||
useSubscription<DepositEventSubscription, DepositEventSubscriptionVariables>(
|
||||
DepositEventDocument,
|
||||
{
|
||||
variables: { partyId: partyId ? remove0x(partyId) : '' },
|
||||
skip: !partyId,
|
||||
onSubscriptionData: ({ subscriptionData }) => {
|
||||
if (!subscriptionData.data?.busEvents?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingDeposit = subscriptionData.data.busEvents.find((e) => {
|
||||
if (e.event.__typename !== 'Deposit') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
e.event.txHash === transaction.transaction.txHash &&
|
||||
// Note there is a bug in data node where the subscription is not emitted when the status
|
||||
// changes from 'Open' to 'Finalized' as a result the deposit UI will hang in a pending state right now
|
||||
// https://github.com/vegaprotocol/data-node/issues/460
|
||||
e.event.status === Schema.DepositStatus.STATUS_FINALIZED
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (matchingDeposit && matchingDeposit.event.__typename === 'Deposit') {
|
||||
transaction.setConfirmed();
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
perform: async (args: {
|
||||
assetSource: string;
|
||||
amount: string;
|
||||
vegaPublicKey: string;
|
||||
}) => {
|
||||
if (!asset) return;
|
||||
try {
|
||||
setPartyId(args.vegaPublicKey);
|
||||
const publicKey = prepend0x(args.vegaPublicKey);
|
||||
const amount = removeDecimal(args.amount, asset.decimals);
|
||||
await transaction.perform(args.assetSource, amount, publicKey);
|
||||
const balance = await getBalance();
|
||||
update({ balance });
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
@ -1,17 +1,14 @@
|
||||
import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3';
|
||||
import { useDepositStore } from './deposit-store';
|
||||
import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
|
||||
import { isAssetTypeERC20 } from '@vegaprotocol/react-helpers';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
|
||||
export const useSubmitFaucet = () => {
|
||||
const { asset, update } = useDepositStore();
|
||||
export const useSubmitFaucet = (asset?: Asset) => {
|
||||
const contract = useTokenContract(
|
||||
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
|
||||
true
|
||||
);
|
||||
const getBalance = useGetBalanceOfERC20Token(contract, asset);
|
||||
const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>(
|
||||
contract,
|
||||
'faucet'
|
||||
@ -21,8 +18,6 @@ export const useSubmitFaucet = () => {
|
||||
perform: async () => {
|
||||
try {
|
||||
await transaction.perform();
|
||||
const balance = await getBalance();
|
||||
update({ balance });
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
|
@ -65,9 +65,7 @@ export const useUpdateNetworkParametersToasts = (): Toast[] => {
|
||||
return {
|
||||
id: `update-network-param-proposal-${proposal.id}`,
|
||||
intent: Intent.Warning,
|
||||
render: () => (
|
||||
<UpdateNetworkParameterToastContent proposal={proposal} />
|
||||
),
|
||||
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
|
||||
onClose: () => remove(id),
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
|
@ -22,6 +22,12 @@ fragment OrderFields on Order {
|
||||
}
|
||||
}
|
||||
|
||||
query OrderById($orderId: ID!) {
|
||||
orderByID(id: $orderId) {
|
||||
...OrderFields
|
||||
}
|
||||
}
|
||||
|
||||
query Orders(
|
||||
$partyId: ID!
|
||||
$pagination: Pagination
|
||||
|
@ -5,6 +5,13 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type OrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null };
|
||||
|
||||
export type OrderByIdQueryVariables = Types.Exact<{
|
||||
orderId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type OrderByIdQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder' } | null } };
|
||||
|
||||
export type OrdersQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
pagination?: Types.InputMaybe<Types.Pagination>;
|
||||
@ -72,6 +79,41 @@ export const OrderUpdateFieldsFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const OrderByIdDocument = gql`
|
||||
query OrderById($orderId: ID!) {
|
||||
orderByID(id: $orderId) {
|
||||
...OrderFields
|
||||
}
|
||||
}
|
||||
${OrderFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useOrderByIdQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useOrderByIdQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useOrderByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useOrderByIdQuery({
|
||||
* variables: {
|
||||
* orderId: // value for 'orderId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useOrderByIdQuery(baseOptions: Apollo.QueryHookOptions<OrderByIdQuery, OrderByIdQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<OrderByIdQuery, OrderByIdQueryVariables>(OrderByIdDocument, options);
|
||||
}
|
||||
export function useOrderByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<OrderByIdQuery, OrderByIdQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<OrderByIdQuery, OrderByIdQueryVariables>(OrderByIdDocument, options);
|
||||
}
|
||||
export type OrderByIdQueryHookResult = ReturnType<typeof useOrderByIdQuery>;
|
||||
export type OrderByIdLazyQueryHookResult = ReturnType<typeof useOrderByIdLazyQuery>;
|
||||
export type OrderByIdQueryResult = Apollo.QueryResult<OrderByIdQuery, OrderByIdQueryVariables>;
|
||||
export const OrdersDocument = gql`
|
||||
query Orders($partyId: ID!, $pagination: Pagination, $dateRange: DateRange, $filter: OrderFilter, $marketId: ID) {
|
||||
party(id: $partyId) {
|
||||
|
@ -76,7 +76,9 @@ export const OrderFeedback = ({ transaction, order }: OrderFeedbackProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getRejectionReason = (order: OrderEventFieldsFragment): string | null => {
|
||||
export const getRejectionReason = (
|
||||
order: OrderEventFieldsFragment
|
||||
): string | null => {
|
||||
switch (order.status) {
|
||||
case Schema.OrderStatus.STATUS_STOPPED:
|
||||
return t(
|
||||
|
@ -17,11 +17,11 @@ import type { Filter, Sort } from './use-order-list-data';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import type { TransactionResult } from '@vegaprotocol/wallet';
|
||||
import type { VegaTxState } from '@vegaprotocol/wallet';
|
||||
import { useOrderCancel } from '../../order-hooks/use-order-cancel';
|
||||
import { useOrderEdit } from '../../order-hooks/use-order-edit';
|
||||
import { OrderFeedback } from '../order-feedback';
|
||||
import {
|
||||
normalizeOrderAmendment,
|
||||
useVegaTransactionStore,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { VegaTxState, TransactionResult } from '@vegaprotocol/wallet';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { OrderEventFieldsFragment } from '../../order-hooks';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@ -76,8 +76,7 @@ export const OrderListManager = ({
|
||||
const [sort, setSort] = useState<Sort[] | undefined>();
|
||||
const [filter, setFilter] = useState<Filter | undefined>();
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
const orderCancel = useOrderCancel();
|
||||
const orderEdit = useOrderEdit(editOrder);
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
const hasActiveOrder = useHasActiveOrder(marketId);
|
||||
|
||||
const { data, error, loading, addNewRows, getRows } = useOrderListData({
|
||||
@ -136,9 +135,11 @@ export const OrderListManager = ({
|
||||
onSortChanged={onSortChange}
|
||||
cancel={(order: Order) => {
|
||||
if (!order.market) return;
|
||||
orderCancel.cancel({
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
create({
|
||||
orderCancellation: {
|
||||
orderId: order.id,
|
||||
marketId: order.market.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
setEditOrder={setEditOrder}
|
||||
@ -157,7 +158,13 @@ export const OrderListManager = ({
|
||||
<div className="w-full dark:bg-black bg-white absolute bottom-0 h-auto flex justify-end px-[11px] py-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => orderCancel.cancel({ marketId })}
|
||||
onClick={() => {
|
||||
create({
|
||||
orderCancellation: {
|
||||
marketId,
|
||||
},
|
||||
});
|
||||
}}
|
||||
data-testid="cancelAll"
|
||||
>
|
||||
{t('Cancel all')}
|
||||
@ -165,31 +172,7 @@ export const OrderListManager = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<orderCancel.Dialog
|
||||
title={getCancelDialogTitle(orderCancel)}
|
||||
intent={getCancelDialogIntent(orderCancel)}
|
||||
content={{
|
||||
Complete: orderCancel.cancelledOrder ? (
|
||||
<OrderFeedback
|
||||
transaction={orderCancel.transaction}
|
||||
order={orderCancel.cancelledOrder}
|
||||
/>
|
||||
) : (
|
||||
<TransactionComplete {...orderCancel} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<orderEdit.Dialog
|
||||
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||
content={{
|
||||
Complete: (
|
||||
<OrderFeedback
|
||||
transaction={orderEdit.transaction}
|
||||
order={orderEdit.updatedOrder}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
isOpen={Boolean(editOrder)}
|
||||
@ -198,8 +181,17 @@ export const OrderListManager = ({
|
||||
}}
|
||||
order={editOrder}
|
||||
onSubmit={(fields) => {
|
||||
if (!editOrder.market) {
|
||||
return;
|
||||
}
|
||||
const orderAmendment = normalizeOrderAmendment(
|
||||
editOrder,
|
||||
editOrder.market,
|
||||
fields.limitPrice,
|
||||
fields.size
|
||||
);
|
||||
create({ orderAmendment });
|
||||
setEditOrder(null);
|
||||
orderEdit.edit({ price: fields.limitPrice, size: fields.size });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -9,7 +9,6 @@ import type { OrderEventSubscription } from './';
|
||||
import { OrderEventDocument } from './';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { toNanoSeconds } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const defaultMarket = {
|
||||
__typename: 'Market',
|
||||
@ -180,7 +179,7 @@ describe('useOrderSubmit', () => {
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
price: '123456789',
|
||||
expiresAt: toNanoSeconds(order.expiresAt),
|
||||
expiresAt: new Date('2022-01-01').toISOString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -217,7 +216,7 @@ describe('useOrderSubmit', () => {
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
price: '123456789',
|
||||
expiresAt: undefined,
|
||||
expiresAt: new Date('2022-01-01').toISOString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { OrderEventFieldsFragment } from './__generated__/OrderEvent';
|
||||
import { toNanoSeconds } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useVegaWallet,
|
||||
useVegaTransaction,
|
||||
@ -108,28 +107,15 @@ export const useOrderSubmit = () => {
|
||||
}, [resetTransaction]);
|
||||
|
||||
const submit = useCallback(
|
||||
async (order: OrderSubmissionBody['orderSubmission']) => {
|
||||
if (!pubKey || !order.side) {
|
||||
async (orderSubmission: OrderSubmissionBody['orderSubmission']) => {
|
||||
if (!pubKey || !orderSubmission.side) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFinalizedOrder(null);
|
||||
|
||||
try {
|
||||
const res = await send(pubKey, {
|
||||
orderSubmission: {
|
||||
...order,
|
||||
price:
|
||||
order.type === Schema.OrderType.TYPE_LIMIT && order.price
|
||||
? order.price
|
||||
: undefined,
|
||||
expiresAt:
|
||||
order.expiresAt &&
|
||||
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
const res = await send(pubKey, { orderSubmission });
|
||||
|
||||
if (res) {
|
||||
const orderId = determineId(res.signature);
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { useRef } from 'react';
|
||||
import { AsyncRenderer, Icon, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useClosePosition, usePositionsData, PositionsTable } from '../';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { usePositionsData, PositionsTable } from '../';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { Requested } from './close-position-dialog/requested';
|
||||
import { Complete } from './close-position-dialog/complete';
|
||||
import type { TransactionResult } from '@vegaprotocol/wallet';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface PositionsManagerProps {
|
||||
@ -14,14 +13,7 @@ interface PositionsManagerProps {
|
||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef);
|
||||
const {
|
||||
submit,
|
||||
closingOrder,
|
||||
closingOrderResult,
|
||||
transaction,
|
||||
transactionResult,
|
||||
Dialog,
|
||||
} = useClosePosition();
|
||||
const create = useVegaTransactionStore((store) => store.create);
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
@ -29,7 +21,30 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
rowModelType="infinite"
|
||||
ref={gridRef}
|
||||
datasource={{ getRows }}
|
||||
onClose={(position) => submit(position)}
|
||||
onClose={({ marketId, openVolume }) =>
|
||||
create({
|
||||
batchMarketInstructions: {
|
||||
cancellations: [
|
||||
{
|
||||
marketId,
|
||||
orderId: '', // omit order id to cancel all active orders
|
||||
},
|
||||
],
|
||||
submissions: [
|
||||
{
|
||||
marketId: marketId,
|
||||
type: Schema.OrderType.TYPE_MARKET as const,
|
||||
timeInForce: Schema.OrderTimeInForce
|
||||
.TIME_IN_FORCE_FOK as const,
|
||||
side: openVolume.startsWith('-')
|
||||
? Schema.Side.SIDE_BUY
|
||||
: Schema.Side.SIDE_SELL,
|
||||
size: openVolume.replace('-', ''),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
noRowsOverlayComponent={() => null}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
@ -41,63 +56,6 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
/>
|
||||
</div>
|
||||
<Dialog
|
||||
intent={getDialogIntent(transactionResult)}
|
||||
icon={getDialogIcon(transactionResult)}
|
||||
title={getDialogTitle(transactionResult)}
|
||||
content={{
|
||||
Requested: <Requested partyId={partyId} order={closingOrder} />,
|
||||
Complete: (
|
||||
<Complete
|
||||
partyId={partyId}
|
||||
closingOrder={closingOrder}
|
||||
closingOrderResult={closingOrderResult}
|
||||
transaction={transaction}
|
||||
transactionResult={transactionResult}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getDialogIntent = (transactionResult?: TransactionResult) => {
|
||||
if (!transactionResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
transactionResult &&
|
||||
'error' in transactionResult &&
|
||||
transactionResult.error
|
||||
) {
|
||||
return Intent.Danger;
|
||||
}
|
||||
|
||||
return Intent.Success;
|
||||
};
|
||||
|
||||
const getDialogIcon = (transactionResult?: TransactionResult) => {
|
||||
if (!transactionResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionResult.status) {
|
||||
return <Icon name="tick" />;
|
||||
}
|
||||
|
||||
return <Icon name="error" />;
|
||||
};
|
||||
|
||||
const getDialogTitle = (transactionResult?: TransactionResult) => {
|
||||
if (!transactionResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionResult.status) {
|
||||
return t('Position closed');
|
||||
}
|
||||
|
||||
return t('Position not closed');
|
||||
};
|
||||
|
@ -11,3 +11,4 @@ export * from './use-screen-dimensions';
|
||||
export * from './use-theme-switcher';
|
||||
export * from './use-storybook-theme-observer';
|
||||
export * from './use-yesterday';
|
||||
export * from './use-previous';
|
||||
|
22
libs/react-helpers/src/hooks/use-previous.spec.ts
Normal file
22
libs/react-helpers/src/hooks/use-previous.spec.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { usePrevious } from './use-previous';
|
||||
|
||||
describe('usePrevious', () => {
|
||||
it('returns previous value', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }: { value: string }) => usePrevious(value),
|
||||
{
|
||||
initialProps: {
|
||||
value: 'ABC',
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(result.current).toEqual('ABC');
|
||||
rerender({ value: 'DEF' });
|
||||
expect(result.current).toEqual('ABC');
|
||||
rerender({ value: 'GHI' });
|
||||
expect(result.current).toEqual('DEF');
|
||||
rerender({ value: 'JKL' });
|
||||
expect(result.current).toEqual('GHI');
|
||||
});
|
||||
});
|
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = React.useRef<T | undefined>(value);
|
||||
const ref = useRef<T | undefined>(value);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
@ -7,17 +7,31 @@ export const Size = ({
|
||||
value,
|
||||
side,
|
||||
positionDecimalPlaces = 0,
|
||||
forceTheme,
|
||||
}: {
|
||||
value: string;
|
||||
side: Schema.Side;
|
||||
side?: Schema.Side;
|
||||
positionDecimalPlaces?: number;
|
||||
forceTheme?: 'dark' | 'light';
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
data-testid="size"
|
||||
className={classNames('text-right', {
|
||||
'text-vega-green dark:text-vega-green': side === Schema.Side.SIDE_BUY,
|
||||
'text-vega-pink dark:text-vega-pink': side === Schema.Side.SIDE_SELL,
|
||||
// BUY
|
||||
'text-vega-green-dark dark:text-vega-green':
|
||||
side === Schema.Side.SIDE_BUY && !forceTheme,
|
||||
'text-vega-green-dark':
|
||||
side === Schema.Side.SIDE_BUY && forceTheme === 'light',
|
||||
'text-vega-green':
|
||||
side === Schema.Side.SIDE_BUY && forceTheme === 'dark',
|
||||
// SELL
|
||||
'text-vega-pink-dark dark:text-vega-pink':
|
||||
side === Schema.Side.SIDE_SELL && !forceTheme,
|
||||
'text-vega-pink-dark':
|
||||
side === Schema.Side.SIDE_SELL && forceTheme === 'light',
|
||||
'text-vega-pink':
|
||||
side === Schema.Side.SIDE_SELL && forceTheme === 'dark',
|
||||
})}
|
||||
>
|
||||
{side === Schema.Side.SIDE_BUY
|
||||
|
@ -215,7 +215,7 @@ export const OrderStatusMapping: {
|
||||
STATUS_EXPIRED: 'Expired',
|
||||
STATUS_FILLED: 'Filled',
|
||||
STATUS_PARKED: 'Parked',
|
||||
STATUS_PARTIALLY_FILLED: 'PartiallyFilled',
|
||||
STATUS_PARTIALLY_FILLED: 'Partially Filled',
|
||||
STATUS_REJECTED: 'Rejected',
|
||||
STATUS_STOPPED: 'Stopped',
|
||||
};
|
||||
|
@ -9,18 +9,14 @@ export default {
|
||||
} as ComponentMeta<typeof Toast>;
|
||||
|
||||
const Template: ComponentStory<typeof Toast> = (args) => {
|
||||
return (
|
||||
<Toast
|
||||
{...args}
|
||||
render={() => (
|
||||
<>
|
||||
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
|
||||
<p>Eaque exercitationem saepe cupiditate sunt impedit.</p>
|
||||
<p>I really like 🥪🥪🥪!</p>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
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} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
@ -11,15 +11,14 @@ import { Intent } from '../../utils/intent';
|
||||
import { Icon } from '../icon';
|
||||
import { Loader } from '../loader';
|
||||
|
||||
type ToastContentProps = { id: string };
|
||||
type ToastContent = (props: ToastContentProps) => JSX.Element;
|
||||
export type ToastContent = JSX.Element | undefined;
|
||||
|
||||
type ToastState = 'initial' | 'showing' | 'expired';
|
||||
|
||||
export type Toast = {
|
||||
id: string;
|
||||
intent: Intent;
|
||||
render: ToastContent;
|
||||
content: ToastContent;
|
||||
closeAfter?: number;
|
||||
onClose?: () => void;
|
||||
signal?: 'close';
|
||||
@ -52,7 +51,7 @@ export const CLOSE_DELAY = 750;
|
||||
export const Toast = ({
|
||||
id,
|
||||
intent,
|
||||
render,
|
||||
content,
|
||||
closeAfter,
|
||||
signal,
|
||||
state = 'initial',
|
||||
@ -137,7 +136,7 @@ export const Toast = ({
|
||||
className="flex-1 p-2 pr-6 text-sm overflow-auto"
|
||||
data-testid="toast-content"
|
||||
>
|
||||
{render({ id })}
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,17 +23,17 @@ describe('ToastsContainer', () => {
|
||||
add({
|
||||
id: 'toast-a',
|
||||
intent: Intent.None,
|
||||
render: () => <p>A</p>,
|
||||
content: <p>A</p>,
|
||||
});
|
||||
add({
|
||||
id: 'toast-b',
|
||||
intent: Intent.None,
|
||||
render: () => <p>B</p>,
|
||||
content: <p>B</p>,
|
||||
});
|
||||
add({
|
||||
id: 'toast-c',
|
||||
intent: Intent.None,
|
||||
render: () => <p>C</p>,
|
||||
content: <p>C</p>,
|
||||
});
|
||||
});
|
||||
const { baseElement } = render(
|
||||
@ -54,17 +54,17 @@ describe('ToastsContainer', () => {
|
||||
add({
|
||||
id: 'toast-a',
|
||||
intent: Intent.None,
|
||||
render: () => <p>A</p>,
|
||||
content: <p>A</p>,
|
||||
});
|
||||
add({
|
||||
id: 'toast-b',
|
||||
intent: Intent.None,
|
||||
render: () => <p>B</p>,
|
||||
content: <p>B</p>,
|
||||
});
|
||||
add({
|
||||
id: 'toast-c',
|
||||
intent: Intent.None,
|
||||
render: () => <p>C</p>,
|
||||
content: <p>C</p>,
|
||||
});
|
||||
});
|
||||
const { baseElement } = render(
|
||||
@ -90,13 +90,13 @@ describe('ToastsContainer', () => {
|
||||
add({
|
||||
id: 'toast-a',
|
||||
intent: Intent.None,
|
||||
render: () => <p>A</p>,
|
||||
content: <p>A</p>,
|
||||
onClose: () => remove('toast-a'),
|
||||
});
|
||||
add({
|
||||
id: 'toast-b',
|
||||
intent: Intent.None,
|
||||
render: () => <p>B</p>,
|
||||
content: <p>B</p>,
|
||||
onClose: () => remove('toast-b'),
|
||||
});
|
||||
});
|
||||
|
@ -69,7 +69,7 @@ const randomToast = (): Toast => {
|
||||
Intent.Danger,
|
||||
Intent.Success,
|
||||
]) as Intent,
|
||||
render: () => <p>{content}</p>,
|
||||
content: <p>{content}</p>,
|
||||
closeAfter: sample([undefined, random(1000, 5000)]),
|
||||
};
|
||||
};
|
||||
@ -114,20 +114,20 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
|
||||
];
|
||||
add({
|
||||
...t,
|
||||
render: ({ id }) => (
|
||||
content: (
|
||||
<>
|
||||
<h1>{words[0]}</h1>
|
||||
<div>
|
||||
<button
|
||||
className="underline text-gray-600 mr-2"
|
||||
onClick={() => setTimeout(() => close(id), 500)}
|
||||
onClick={() => setTimeout(() => close(t.id), 500)}
|
||||
>
|
||||
{words[1]}
|
||||
</button>
|
||||
<button
|
||||
className="underline text-gray-600"
|
||||
onClick={() =>
|
||||
update(id, {
|
||||
update(t.id, {
|
||||
intent: sample([
|
||||
Intent.Danger,
|
||||
Intent.Warning,
|
||||
@ -164,7 +164,7 @@ const Template: ComponentStory<typeof ToastsContainer> = (args) => {
|
||||
};
|
||||
add({
|
||||
...t,
|
||||
render: () => <ToastContent />,
|
||||
content: <ToastContent />,
|
||||
onClose: () => remove(t.id),
|
||||
});
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ export const ToastsContainer = ({
|
||||
return (
|
||||
<ul
|
||||
className={classNames(
|
||||
'absolute top-0 right-0 pt-2 pr-2 max-w-full z-20 max-h-full overflow-auto',
|
||||
'absolute top-0 right-0 pt-2 pr-2 max-w-full z-20 max-h-full overflow-x-hidden overflow-y-auto',
|
||||
{
|
||||
'flex flex-col-reverse': order === 'desc',
|
||||
}
|
||||
|
@ -71,6 +71,15 @@ fragment OrderBusEventFields on Order {
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
libs/wallet/src/__generated__/TransactionResult.ts
generated
13
libs/wallet/src/__generated__/TransactionResult.ts
generated
@ -21,14 +21,14 @@ export type WithdrawalBusEventSubscriptionVariables = Types.Exact<{
|
||||
|
||||
export type WithdrawalBusEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', 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' } | { __typename?: 'Withdrawal', id: string, status: Types.WithdrawalStatus, amount: string, createdTimestamp: any, withdrawnTimestamp?: any | null, txHash?: string | null, pendingOnForeignChain: boolean, asset: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, status: Types.AssetStatus, source: { __typename?: 'BuiltinAsset' } | { __typename?: 'ERC20', contractAddress: string } }, details?: { __typename?: 'Erc20WithdrawalDetails', receiverAddress: string } | null } }> | null };
|
||||
|
||||
export type OrderBusEventFieldsFragment = { __typename?: 'Order', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } };
|
||||
export type OrderBusEventFieldsFragment = { __typename?: 'Order', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename?: 'Future', settlementAsset: { __typename?: 'Asset', symbol: string, decimals: number } } } } } };
|
||||
|
||||
export type OrderBusEventsSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type OrderBusEventsSubscription = { __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', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string } } } } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal' } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote' } | { __typename?: 'Withdrawal' } }> | null };
|
||||
export type OrderBusEventsSubscription = { __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', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename?: 'Future', settlementAsset: { __typename?: 'Asset', symbol: string, decimals: number } } } } } } | { __typename?: 'Party' } | { __typename?: 'PositionResolution' } | { __typename?: 'Proposal' } | { __typename?: 'RiskFactor' } | { __typename?: 'SettleDistressed' } | { __typename?: 'SettlePosition' } | { __typename?: 'TimeUpdate' } | { __typename?: 'Trade' } | { __typename?: 'TransactionResult' } | { __typename?: 'TransferResponses' } | { __typename?: 'Vote' } | { __typename?: 'Withdrawal' } }> | null };
|
||||
|
||||
export type DepositBusEventFieldsFragment = { __typename?: 'Deposit', id: string, status: Types.DepositStatus, amount: string, createdTimestamp: any, creditedTimestamp?: any | null, txHash?: string | null, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } };
|
||||
|
||||
@ -94,6 +94,15 @@ export const OrderBusEventFieldsFragmentDoc = gql`
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { WalletClient } from '@vegaprotocol/wallet-client';
|
||||
import { WalletClient, WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
import { clearConfig, getConfig, setConfig } from '../storage';
|
||||
import type { Transaction, VegaConnector } from './vega-connector';
|
||||
import { WalletError } from './vega-connector';
|
||||
@ -21,16 +21,14 @@ export const ClientErrors = {
|
||||
105,
|
||||
t('Unknown error occurred')
|
||||
),
|
||||
NO_CLIENT: new WalletError(t('No client found.'), 106),
|
||||
REQUEST_REJECTED: new WalletError(
|
||||
t('Request rejected'),
|
||||
107,
|
||||
t('The request has been rejected by the user')
|
||||
),
|
||||
} as const;
|
||||
|
||||
class NoClientError extends Error {
|
||||
constructor() {
|
||||
super(
|
||||
t('No client found. The connector needs to be initialized with a url.')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonRpcConnector implements VegaConnector {
|
||||
version = VERSION;
|
||||
private _url: string | null = null;
|
||||
@ -62,7 +60,7 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
|
||||
async getChainId() {
|
||||
if (!this.client) {
|
||||
throw new NoClientError();
|
||||
throw ClientErrors.NO_CLIENT;
|
||||
}
|
||||
try {
|
||||
const { result } = await this.client.GetChainId();
|
||||
@ -74,7 +72,7 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
|
||||
async connectWallet() {
|
||||
if (!this.client) {
|
||||
throw new NoClientError();
|
||||
throw ClientErrors.NO_CLIENT;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -86,7 +84,11 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
throw ClientErrors.INVALID_RESPONSE;
|
||||
const clientErr =
|
||||
err instanceof WalletClientError && err.code === 3001
|
||||
? ClientErrors.REQUEST_REJECTED
|
||||
: ClientErrors.INVALID_RESPONSE;
|
||||
throw clientErr;
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +96,7 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
// which retrieves the session token
|
||||
async connect() {
|
||||
if (!this.client) {
|
||||
throw new NoClientError();
|
||||
throw ClientErrors.NO_CLIENT;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -107,7 +109,7 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
|
||||
async disconnect() {
|
||||
if (!this.client) {
|
||||
throw new NoClientError();
|
||||
throw ClientErrors.NO_CLIENT;
|
||||
}
|
||||
|
||||
await this.client.DisconnectWallet();
|
||||
@ -116,25 +118,21 @@ export class JsonRpcConnector implements VegaConnector {
|
||||
|
||||
async sendTx(pubKey: string, transaction: Transaction) {
|
||||
if (!this.client) {
|
||||
throw new NoClientError();
|
||||
throw ClientErrors.NO_CLIENT;
|
||||
}
|
||||
|
||||
try {
|
||||
const { result } = await this.client.SendTransaction({
|
||||
publicKey: pubKey,
|
||||
sendingMode: 'TYPE_SYNC',
|
||||
transaction,
|
||||
});
|
||||
const { result } = await this.client.SendTransaction({
|
||||
publicKey: pubKey,
|
||||
sendingMode: 'TYPE_SYNC',
|
||||
transaction,
|
||||
});
|
||||
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
sentAt: result.sentAt,
|
||||
receivedAt: result.receivedAt,
|
||||
signature: result.transaction.signature.value,
|
||||
};
|
||||
} catch (err) {
|
||||
throw ClientErrors.INVALID_RESPONSE;
|
||||
}
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
sentAt: result.sentAt,
|
||||
receivedAt: result.receivedAt,
|
||||
signature: result.transaction.signature.value,
|
||||
};
|
||||
}
|
||||
|
||||
async checkCompat() {
|
||||
|
@ -318,6 +318,11 @@ export const isOrderAmendmentTransaction = (
|
||||
transaction: Transaction
|
||||
): transaction is OrderAmendmentBody => 'orderAmendment' in transaction;
|
||||
|
||||
export const isBatchMarketInstructionsTransaction = (
|
||||
transaction: Transaction
|
||||
): transaction is BatchMarketInstructionSubmissionBody =>
|
||||
'batchMarketInstructions' in transaction;
|
||||
|
||||
export interface TransactionResponse {
|
||||
transactionHash: string;
|
||||
signature: string; // still to be added by core
|
||||
|
@ -4,6 +4,7 @@ import { ClientErrors } from './connectors';
|
||||
import { WalletError } from './connectors';
|
||||
import { VegaTxStatus } from './use-vega-transaction';
|
||||
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
||||
import { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
|
||||
export const useVegaTransactionManager = () => {
|
||||
const { sendTx, pubKey } = useVegaWallet();
|
||||
@ -39,7 +40,10 @@ export const useVegaTransactionManager = () => {
|
||||
})
|
||||
.catch((err) => {
|
||||
update(transaction.id, {
|
||||
error: err instanceof WalletError ? err : ClientErrors.UNKNOWN,
|
||||
error:
|
||||
err instanceof WalletError || err instanceof WalletClientError
|
||||
? err
|
||||
: ClientErrors.UNKNOWN,
|
||||
status: VegaTxStatus.Error,
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
isOrderSubmissionTransaction,
|
||||
isOrderCancellationTransaction,
|
||||
isOrderAmendmentTransaction,
|
||||
isBatchMarketInstructionsTransaction,
|
||||
} from './connectors';
|
||||
import { determineId } from './utils';
|
||||
|
||||
@ -126,17 +127,42 @@ export const useVegaTransactionStore = create<VegaTransactionStore>(
|
||||
updateOrder: (order: OrderBusEventFieldsFragment) => {
|
||||
set(
|
||||
produce((state: VegaTransactionStore) => {
|
||||
const transaction = state.transactions.find(
|
||||
(transaction) =>
|
||||
transaction &&
|
||||
transaction.status === VegaTxStatus.Pending &&
|
||||
const transaction = state.transactions.find((transaction) => {
|
||||
if (!transaction || transaction.status !== VegaTxStatus.Pending) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
isOrderSubmissionTransaction(transaction?.body) &&
|
||||
transaction.signature &&
|
||||
(isOrderSubmissionTransaction(transaction?.body) ||
|
||||
isOrderCancellationTransaction(transaction?.body) ||
|
||||
isOrderAmendmentTransaction(transaction?.body)) &&
|
||||
order.id === determineId(transaction.signature)
|
||||
);
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isOrderCancellationTransaction(transaction?.body) &&
|
||||
order.id === transaction.body.orderCancellation.orderId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isOrderAmendmentTransaction(transaction?.body) &&
|
||||
order.id === transaction.body.orderAmendment.orderId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isBatchMarketInstructionsTransaction(transaction?.body) &&
|
||||
transaction.signature &&
|
||||
order.id === determineId(transaction.signature)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (transaction) {
|
||||
// TODO: handle multiple orders from batch market instructions
|
||||
// Note: If multiple orders are submitted the first order ID is determined by hashing the signature of the transaction
|
||||
// (see determineId function). For each subsequent order's ID, a hash of the previous orders ID is used
|
||||
transaction.order = order;
|
||||
transaction.status = VegaTxStatus.Complete;
|
||||
transaction.dialogOpen = true;
|
||||
@ -158,6 +184,14 @@ export const useVegaTransactionStore = create<VegaTransactionStore>(
|
||||
);
|
||||
if (transaction) {
|
||||
transaction.transactionResult = transactionResult;
|
||||
if (
|
||||
isOrderCancellationTransaction(transaction.body) &&
|
||||
!transaction.body.orderCancellation.orderId &&
|
||||
!transactionResult.error &&
|
||||
transactionResult.status
|
||||
) {
|
||||
transaction.status = VegaTxStatus.Complete;
|
||||
}
|
||||
transaction.dialogOpen = true;
|
||||
transaction.updatedAt = new Date();
|
||||
}
|
||||
|
@ -87,6 +87,15 @@ const orderBusEvent: OrderBusEventFieldsFragment = {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||
code: 'UNIDAI',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
settlementAsset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 8,
|
||||
symbol: 'AAA',
|
||||
},
|
||||
},
|
||||
__typename: 'Instrument',
|
||||
},
|
||||
},
|
||||
|
@ -7,6 +7,7 @@ import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transaction } from './connectors';
|
||||
import { ClientErrors } from './connectors';
|
||||
import { WalletError } from './connectors';
|
||||
import type { WalletClientError } from '@vegaprotocol/wallet-client';
|
||||
|
||||
export interface DialogProps {
|
||||
intent?: Intent;
|
||||
@ -25,7 +26,7 @@ export enum VegaTxStatus {
|
||||
|
||||
export interface VegaTxState {
|
||||
status: VegaTxStatus;
|
||||
error: WalletError | Error | null;
|
||||
error: WalletError | WalletClientError | Error | null;
|
||||
txHash: string | null;
|
||||
signature: string | null;
|
||||
dialogOpen: boolean;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useContext } from 'react';
|
||||
import { VegaWalletContext } from '.';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useVegaWalletDialogStore, VegaWalletContext } from '.';
|
||||
|
||||
export function useVegaWallet() {
|
||||
const context = useContext(VegaWalletContext);
|
||||
@ -8,3 +8,16 @@ export function useVegaWallet() {
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useReconnectVegaWallet() {
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
const { disconnect } = useVegaWallet();
|
||||
const reconnect = useCallback(async () => {
|
||||
await disconnect();
|
||||
openVegaWalletDialog();
|
||||
}, [disconnect, openVegaWalletDialog]);
|
||||
|
||||
return reconnect;
|
||||
}
|
||||
|
@ -1,10 +1,127 @@
|
||||
import { determineId } from './utils';
|
||||
|
||||
it('produces a known result for an ID', () => {
|
||||
const res = determineId(
|
||||
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909'
|
||||
);
|
||||
expect(res).toStrictEqual(
|
||||
'2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50'
|
||||
);
|
||||
import {
|
||||
determineId,
|
||||
normalizeOrderAmendment,
|
||||
normalizeOrderSubmission,
|
||||
} from './utils';
|
||||
import type { OrderSubmissionBody } from './connectors/vega-connector';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
describe('determineId', () => {
|
||||
it('produces a known result for an ID', () => {
|
||||
const res = determineId(
|
||||
'cfe592d169f87d0671dd447751036d0dddc165b9c4b65e5a5060e2bbadd1aa726d4cbe9d3c3b327bcb0bff4f83999592619a2493f9bbd251fae99ce7ce766909'
|
||||
);
|
||||
expect(res).toStrictEqual(
|
||||
'2fca514cebf9f465ae31ecb4c5721e3a6f5f260425ded887ca50ba15b81a5d50'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOrderSubmission', () => {
|
||||
it('sets and formats price only for limit orders', () => {
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{ price: '100' } as unknown as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).price
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
price: '100',
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
} as unknown as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).price
|
||||
).toEqual('10000');
|
||||
});
|
||||
|
||||
it('sets and formats expiresAt only for time in force orders', () => {
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
expiresAt: '2022-01-01T00:00:00.000Z',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).expiresAt
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
expiresAt: '2022-01-01T00:00:00.000Z',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).expiresAt
|
||||
).toEqual('1640995200000000000');
|
||||
});
|
||||
|
||||
it('formats size', () => {
|
||||
expect(
|
||||
normalizeOrderSubmission(
|
||||
{
|
||||
size: '100',
|
||||
} as OrderSubmissionBody['orderSubmission'],
|
||||
2,
|
||||
1
|
||||
).size
|
||||
).toEqual('1000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOrderAmendment', () => {
|
||||
type Order = Parameters<typeof normalizeOrderAmendment>[0];
|
||||
type Market = Parameters<typeof normalizeOrderAmendment>[1];
|
||||
const order: Order = {
|
||||
id: '123',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
|
||||
size: '100',
|
||||
expiresAt: '2022-01-01T00:00:00.000Z',
|
||||
};
|
||||
const market: Market = {
|
||||
id: '456',
|
||||
decimalPlaces: 1,
|
||||
positionDecimalPlaces: 1,
|
||||
};
|
||||
|
||||
it('sets and formats order id, market id, expires and timeInForce as given', () => {
|
||||
const orderAmendment = normalizeOrderAmendment(order, market, '1', '1');
|
||||
expect(orderAmendment.orderId).toEqual('123');
|
||||
expect(orderAmendment.marketId).toEqual('456');
|
||||
expect(orderAmendment.expiresAt).toEqual('1640995200000000000');
|
||||
expect(orderAmendment.timeInForce).toEqual(
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['1.1', 1, '11'],
|
||||
['1.1', 2, '110'],
|
||||
['0.001', 8, '100000'],
|
||||
])('sets and formats price', (price, decimalPlaces, output) => {
|
||||
const orderAmendment = normalizeOrderAmendment(
|
||||
order,
|
||||
{ ...market, decimalPlaces },
|
||||
price,
|
||||
'1'
|
||||
);
|
||||
expect(orderAmendment.price).toEqual(output);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['9', 1, -10],
|
||||
['90', 2, 8900],
|
||||
['0.001', 8, 99900],
|
||||
])('sets and formats size delta', (size, positionDecimalPlaces, output) => {
|
||||
const orderAmendment = normalizeOrderAmendment(
|
||||
order,
|
||||
{ ...market, positionDecimalPlaces },
|
||||
'1',
|
||||
size
|
||||
);
|
||||
expect(orderAmendment.sizeDelta).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,14 @@
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/react-helpers';
|
||||
import type { Market, Order } from '@vegaprotocol/types';
|
||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { ethers } from 'ethers';
|
||||
import { sha3_256 } from 'js-sha3';
|
||||
import type { Transaction } from './connectors';
|
||||
import type {
|
||||
OrderAmendmentBody,
|
||||
OrderSubmissionBody,
|
||||
Transaction,
|
||||
} from './connectors';
|
||||
|
||||
/**
|
||||
* Creates an ID in the same way that core does on the backend. This way we
|
||||
@ -18,3 +26,40 @@ export const encodeTransaction = (tx: Transaction): string => {
|
||||
ethers.utils.toUtf8Bytes(JSON.stringify(tx))
|
||||
);
|
||||
};
|
||||
|
||||
export const normalizeOrderSubmission = (
|
||||
order: OrderSubmissionBody['orderSubmission'],
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number
|
||||
): OrderSubmissionBody['orderSubmission'] => ({
|
||||
...order,
|
||||
price:
|
||||
order.type === OrderType.TYPE_LIMIT && order.price
|
||||
? removeDecimal(order.price, decimalPlaces)
|
||||
: undefined,
|
||||
size: removeDecimal(order.size, positionDecimalPlaces),
|
||||
expiresAt:
|
||||
order.expiresAt && order.timeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||
? toNanoSeconds(order.expiresAt)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export const normalizeOrderAmendment = (
|
||||
order: Pick<Order, 'id' | 'timeInForce' | 'size' | 'expiresAt'>,
|
||||
market: Pick<Market, 'id' | 'decimalPlaces' | 'positionDecimalPlaces'>,
|
||||
price: string,
|
||||
size: string
|
||||
): OrderAmendmentBody['orderAmendment'] => ({
|
||||
orderId: order.id,
|
||||
marketId: market.id,
|
||||
price: removeDecimal(price, market.decimalPlaces),
|
||||
timeInForce: order.timeInForce,
|
||||
sizeDelta: size
|
||||
? new BigNumber(removeDecimal(size, market.positionDecimalPlaces))
|
||||
.minus(order.size)
|
||||
.toNumber()
|
||||
: 0,
|
||||
expiresAt: order.expiresAt
|
||||
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
||||
: undefined,
|
||||
});
|
||||
|
@ -21,19 +21,19 @@ export interface EthStoredTxState extends EthTxState {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
contract: Contract;
|
||||
contract: Contract | null;
|
||||
methodName: ContractMethod;
|
||||
args: string[];
|
||||
requiredConfirmations: number;
|
||||
requiresConfirmation: boolean;
|
||||
asset?: string;
|
||||
assetId?: string;
|
||||
deposit?: DepositBusEventFieldsFragment;
|
||||
}
|
||||
|
||||
export interface EthTransactionStore {
|
||||
transactions: (EthStoredTxState | undefined)[];
|
||||
create: (
|
||||
contract: Contract,
|
||||
contract: Contract | null,
|
||||
methodName: ContractMethod,
|
||||
args: string[],
|
||||
assetId?: string,
|
||||
@ -58,10 +58,10 @@ export const useEthTransactionStore = create<EthTransactionStore>(
|
||||
(set, get) => ({
|
||||
transactions: [] as EthStoredTxState[],
|
||||
create: (
|
||||
contract: Contract,
|
||||
contract: Contract | null,
|
||||
methodName: ContractMethod,
|
||||
args: string[] = [],
|
||||
asset,
|
||||
assetId?: string,
|
||||
requiredConfirmations = 1,
|
||||
requiresConfirmation = false
|
||||
) => {
|
||||
@ -82,7 +82,7 @@ export const useEthTransactionStore = create<EthTransactionStore>(
|
||||
dialogOpen: true,
|
||||
requiredConfirmations,
|
||||
requiresConfirmation,
|
||||
asset: asset,
|
||||
assetId,
|
||||
};
|
||||
set({ transactions: transactions.concat(transaction) });
|
||||
return transaction.id;
|
||||
|
@ -36,7 +36,7 @@
|
||||
"@sentry/nextjs": "^6.19.3",
|
||||
"@sentry/react": "^6.19.2",
|
||||
"@sentry/tracing": "^6.19.2",
|
||||
"@vegaprotocol/wallet-client": "0.1.4",
|
||||
"@vegaprotocol/wallet-client": "0.1.5",
|
||||
"@walletconnect/ethereum-provider": "^1.7.5",
|
||||
"@web3-react/core": "8.0.20-beta.0",
|
||||
"@web3-react/metamask": "8.0.16-beta.0",
|
||||
|
@ -7559,10 +7559,10 @@
|
||||
"@typescript-eslint/types" "5.40.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@vegaprotocol/wallet-client@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@vegaprotocol/wallet-client/-/wallet-client-0.1.4.tgz#202fa1a84dbef57199810383f2887a7ee0afd64c"
|
||||
integrity sha512-uGEbusoi3lwyl7Nn9ovBg9YHrfcH/Rl33KUcLfdMeTX8FZO+n7BVm3ejd00e5RsM/PJlqJ1oW4qkiq7kervxng==
|
||||
"@vegaprotocol/wallet-client@0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@vegaprotocol/wallet-client/-/wallet-client-0.1.5.tgz#9d72a7fc9ceb9767f5119c9b7eebe29a2c30f682"
|
||||
integrity sha512-7FmIBFxissr3h2QsEjvD+HXEXJ3u/oaVUg055IgZ08dmy1+4Nx22BOffFyidLBmaH1xJYjyiqxHnhLGnN5BfwA==
|
||||
dependencies:
|
||||
express "4.18.2"
|
||||
nanoid "3.3.4"
|
||||
|
Loading…
Reference in New Issue
Block a user