diff --git a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-container.tsx b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-container.tsx index aebd8a356..d2e71566a 100644 --- a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-container.tsx +++ b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-container.tsx @@ -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 = ( - + <> {loading ? loader : balance} - + ); return ( diff --git a/apps/token/src/hooks/use-animate-value.ts b/apps/token/src/hooks/use-animate-value.ts index c24a862b0..e2a769081 100644 --- a/apps/token/src/hooks/use-animate-value.ts +++ b/apps/token/src/hooks/use-animate-value.ts @@ -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 diff --git a/apps/trading-e2e/.env b/apps/trading-e2e/.env index d6f7eb7d7..5482d94ac 100644 --- a/apps/trading-e2e/.env +++ b/apps/trading-e2e/.env @@ -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 diff --git a/apps/trading-e2e/.env.capsule b/apps/trading-e2e/.env.capsule index d6f7eb7d7..5482d94ac 100644 --- a/apps/trading-e2e/.env.capsule +++ b/apps/trading-e2e/.env.capsule @@ -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 diff --git a/apps/trading-e2e/src/integration/trading-orders.cy.ts b/apps/trading-e2e/src/integration/trading-orders.cy.ts index af5bbff23..bf0a1bbcc 100644 --- a/apps/trading-e2e/src/integration/trading-orders.cy.ts +++ b/apps/trading-e2e/src/integration/trading-orders.cy.ts @@ -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`) diff --git a/apps/trading-e2e/src/support/order-validation.ts b/apps/trading-e2e/src/support/order-validation.ts index c270de7a3..436ddee5e 100644 --- a/apps/trading-e2e/src/support/order-validation.ts +++ b/apps/trading-e2e/src/support/order-validation.ts @@ -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(); }; diff --git a/apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx b/apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx new file mode 100644 index 000000000..ef5b960de --- /dev/null +++ b/apps/trading/lib/hooks/use-ethereum-transaction-toasts.tsx @@ -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 = ( +
+ + {label} {num} {asset.symbol} + +
+ ); + return ( + <> + {details} + {tx.requiresConfirmation && + [EthTxStatus.Pending].includes(tx.status) && ( +
+ + {t('Awaiting confirmations')}{' '} + {`(${tx.confirmations}/${tx.requiredConfirmations})`} + + +
+ )} + + ); + } + } + + return null; +}; + +type EthTxToastContentProps = { + tx: EthStoredTxState; +}; + +const EthTxRequestedToastContent = ({ tx }: EthTxToastContentProps) => { + return ( +
+

{t('Action required')}

+

+ {t( + 'Please go to your wallet application and approve or reject the transaction.' + )} +

+ +
+ ); +}; + +const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => { + const etherscanLink = useEtherscanLink(); + return ( +
+

{t('Awaiting confirmation')}

+

{t('Please wait for your transaction to be confirmed')}

+ {tx.txHash && ( +

+ + {t('View on Etherscan')} + +

+ )} + +
+ ); +}; + +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 ( +
+

{t('Error occurred')}

+

{errorMessage}

+ +
+ ); +}; + +const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => { + const etherscanLink = useEtherscanLink(); + return ( +
+

{t('Transaction completed')}

+

{t('Your transaction has been completed')}

+ {tx.txHash && ( +

+ + {t('View on Etherscan')} + +

+ )} + +
+ ); +}; + +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 = ; + if (tx.status === EthTxStatus.Requested) { + content = ; + } + if (tx.status === EthTxStatus.Pending) { + content = ; + } + if ( + tx.status === EthTxStatus.Confirmed || + tx.status === EthTxStatus.Complete + ) { + content = ; + } + if (tx.status === EthTxStatus.Error) { + content = ; + } + + 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]); +}; diff --git a/apps/trading/lib/hooks/use-ethereum-withdraw-approval-toasts.tsx b/apps/trading/lib/hooks/use-ethereum-withdraw-approval-toasts.tsx new file mode 100644 index 000000000..b3f7383df --- /dev/null +++ b/apps/trading/lib/hooks/use-ethereum-withdraw-approval-toasts.tsx @@ -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 = ( +
+ + {t('Withdraw')} {num} {tx.withdrawal.asset.symbol} + +
+ ); + return ( +
+ {title.length > 0 &&

{title}

} + + {details} +
+ ); +}; + +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: , + }), + [dismissWithdrawApproval] + ); + + const toasts = useMemo(() => { + return [...compact(withdrawApprovals).map(fromWithdrawalApproval)]; + }, [fromWithdrawalApproval, withdrawApprovals]); + + return toasts; +}; diff --git a/apps/trading/lib/hooks/use-vega-transaction-toasts.spec.tsx b/apps/trading/lib/hooks/use-vega-transaction-toasts.spec.tsx new file mode 100644 index 000000000..6a4d9e3fe --- /dev/null +++ b/apps/trading/lib/hooks/use-vega-transaction-toasts.spec.tsx @@ -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( + + ); + 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(); + expect(queryByTestId('vega-tx-details')?.textContent).toEqual(details); + }); +}); diff --git a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx new file mode 100644 index 000000000..32169978d --- /dev/null +++ b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx @@ -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; +}) => ( +
+
+ {children} +
+
+); + +type SizeAtPriceProps = { + side: Side; + size: string; + price: string | undefined; + meta: { positionDecimalPlaces: number; decimalPlaces: number; asset: string }; +}; +const SizeAtPrice = ({ side, size, price, meta }: SizeAtPriceProps) => { + return ( + <> + {' '} + {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 ( +
+

+ {order + ? t( + `Submit order - ${OrderStatusMapping[order.status].toLowerCase()}` + ) + : t('Submit order')} +

+

{market?.tradableInstrument.instrument.code}

+

+ +

+ {order && order.rejectionReason && ( +

{getRejectionReason(order)}

+ )} +
+ ); +}; + +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 = ( + + ); + + const edited = ( + + ); + + return ( +
+

+ {order + ? t(`Edit order - ${OrderStatusMapping[order.status].toLowerCase()}`) + : t('Edit order')} +

+

{market?.tradableInstrument.instrument.code}

+

+ {original} +

+

{edited}

+ {order && order.rejectionReason && ( +

{getRejectionReason(order)}

+ )} +
+ ); +}; + +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 = ( + + ); + return ( +
+

+ {order + ? t( + `Cancel order - ${OrderStatusMapping[order.status].toLowerCase()}` + ) + : t('Cancel order')} +

+

{market?.tradableInstrument.instrument.code}

+

+ {original} +

+ {order && order.rejectionReason && ( +

{getRejectionReason(order)}

+ )} +
+ ); +}; + +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 ( +
+ {t('Withdraw')} {num} {asset.symbol} +
+ ); + } + } + + if (isOrderSubmissionTransaction(tx.body)) { + return ( + + ); + } + + if (isOrderCancellationTransaction(tx.body)) { + // CANCEL ALL (from Portfolio) + if ( + tx.body.orderCancellation.marketId === undefined && + tx.body.orderCancellation.orderId === undefined + ) { + return
{t('Cancel all orders')}
; + } + + // CANCEL + if ( + tx.body.orderCancellation.orderId && + tx.body.orderCancellation.marketId + ) { + return ( + + ); + } + + // 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 ( +
+ {marketName + ? `${t('Cancel all orders for')} ${marketName}` + : t('Cancel all orders')} +
+ ); + } + } + + if (isOrderAmendmentTransaction(tx.body)) { + return ( + + ); + } + + 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 ( +
+ {t('Close position for')} {market.tradableInstrument.instrument.code} +
+ ); + } + } + + if (isBatchMarketInstructionsTransaction(tx.body)) { + return
{t('Batch market instruction')}
; + } + + return null; +}; + +type VegaTxToastContentProps = { tx: VegaStoredTxState }; + +const VegaTxRequestedToastContent = ({ tx }: VegaTxToastContentProps) => ( +
+

{t('Action required')}

+

+ {t( + 'Please go to your Vega wallet application and approve or reject the transaction.' + )} +

+ +
+); + +const VegaTxPendingToastContentProps = ({ tx }: VegaTxToastContentProps) => { + const explorerLink = useLinks(DApp.Explorer); + return ( +
+

{t('Awaiting confirmation')}

+

{t('Please wait for your transaction to be confirmed')}

+ {tx.txHash && ( +

+ + {t('View in block explorer')} + +

+ )} + +
+ ); +}; + +const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => { + const { createEthWithdrawalApproval } = useEthWithdrawApprovalsStore( + (state) => ({ + createEthWithdrawalApproval: state.create, + }) + ); + const explorerLink = useLinks(DApp.Explorer); + if (isWithdrawTransaction(tx.body)) { + const completeWithdrawalButton = tx.withdrawal && ( +
+ +
+ ); + return ( +
+

{t('Funds unlocked')}

+

{t('Your funds have been unlocked for withdrawal')}

+ {tx.txHash && ( +

+ + {t('View in block explorer')} + +

+ )} + + {completeWithdrawalButton} +
+ ); + } + + return ( +
+

{t('Confirmed')}

+

{t('Your transaction has been confirmed ')}

+ {tx.txHash && ( +

+ + {t('View in block explorer')} + +

+ )} + +
+ ); +}; + +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 ( +
+

{label}

+

{errorMessage}

+ {walletError && ( + + )} + +
+ ); +}; + +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 = ; + } + if (tx.status === VegaTxStatus.Pending) { + content = ; + } + if (tx.status === VegaTxStatus.Complete) { + content = ; + } + if (tx.status === VegaTxStatus.Error) { + content = ; + } + 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; +}; diff --git a/apps/trading/pages/toasts-manager.tsx b/apps/trading/pages/toasts-manager.tsx index ada6f8303..7e8f8d750 100644 --- a/apps/trading/pages/toasts-manager.tsx +++ b/apps/trading/pages/toasts-manager.tsx @@ -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; -}) => { - const num = formatNumber(toBigNum(amount, asset.decimals), asset.decimals); - return ( -
- - {label} {num} {asset.symbol} - -
- ); -}; - -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 ( - - ); - } - } - - 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 ( - <> - - {tx.requiresConfirmation && ( -
- - {t('Awaiting confirmations')}{' '} - {`(${tx.confirmations}/${tx.requiredConfirmations})`} - - -
- )} - - ); - } - } - - 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 = {}; - const defaultValues = { - id: `vega-${tx.id}`, - intent: intentMap[tx.status], - render: () => { - return ; - }, - onClose: () => dismissVegaTransaction(tx.id), - }; - if (tx.status === VegaTxStatus.Requested) { - toast = { - render: () => { - return ( -
-

{t('Action required')}

-

- {t( - 'Please go to your Vega wallet application and approve or reject the transaction.' - )} -

- -
- ); - }, - }; - } - if (tx.status === VegaTxStatus.Pending) { - toast = { - render: () => { - return ( -
-

{t('Awaiting confirmation')}

-

{t('Please wait for your transaction to be confirmed')}

- {tx.txHash && ( -

- - {t('View in block explorer')} - -

- )} - -
- ); - }, - loader: true, - }; - } - if (tx.status === VegaTxStatus.Complete) { - toast = { - render: () => { - if (isWithdrawTransaction(tx.body)) { - const completeWithdrawalButton = tx.withdrawal && ( -
- -
- ); - return ( -
-

{t('Funds unlocked')}

-

{t('Your funds have been unlocked for withdrawal')}

- {tx.txHash && ( -

- - {t('View in block explorer')} - -

- )} - - {completeWithdrawalButton} -
- ); - } - - return ( -
-

{t('Confirmed')}

-

{t('Your transaction has been confirmed ')}

- {tx.txHash && ( -

- - {t('View in block explorer')} - -

- )} - -
- ); - }, - }; - } - if (tx.status === VegaTxStatus.Error) { - toast = { - render: () => { - const error = `${tx.error?.message} ${ - tx.error instanceof WalletError - ? tx.error?.data - ? `: ${tx.error?.data}` - : '' - : '' - }`; - return ( -
-

{t('Error occurred')}

-

{error}

- -
- ); - }, - }; - } - return { - ...defaultValues, - ...toast, - }; - }, - [createEthWithdrawalApproval, dismissVegaTransaction, explorerLink] - ); - - const fromEthTransaction = useCallback( - (tx: EthStoredTxState): Toast => { - let toast: Partial = {}; - const defaultValues = { - id: `eth-${tx.id}`, - intent: intentMap[tx.status], - render: () => { - return ; - }, - onClose: () => dismissEthTransaction(tx.id), - }; - if (tx.status === EthTxStatus.Requested) { - toast = { - render: () => { - return ( -
-

{t('Action required')}

-

- {t( - 'Please go to your wallet application and approve or reject the transaction.' - )} -

- -
- ); - }, - }; - } - if (tx.status === EthTxStatus.Pending) { - toast = { - render: () => { - return ( -
-

{t('Awaiting confirmation')}

-

{t('Please wait for your transaction to be confirmed')}

- {tx.txHash && ( -

- - {t('View on Etherscan')} - -

- )} - -
- ); - }, - loader: true, - }; - } - if (tx.status === EthTxStatus.Confirmed) { - toast = { - render: () => { - return ( -
-

{t('Transaction completed')}

-

{t('Your transaction has been completed')}

- {tx.txHash && ( -

- - {t('View on Etherscan')} - -

- )} - -
- ); - }, - }; - } - 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 ( -
-

{t('Error occurred')}

-

{errorMessage}

- -
- ); - }, - }; - } - 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 ( -
- {title.length > 0 &&

{title}

} - - -
- ); - }, - 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, ]); diff --git a/libs/accounts/src/lib/use-account-balance.tsx b/libs/accounts/src/lib/use-account-balance.tsx index 02538c7d2..1fb706996 100644 --- a/libs/accounts/src/lib/use-account-balance.tsx +++ b/libs/accounts/src/lib/use-account-balance.tsx @@ -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(''); const [accountDecimals, setAccountDecimals] = useState(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 || ''); } diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx index d7cea7c8a..d6ea6b82b 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-button.tsx @@ -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 ? (
) : ( diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx index 11c0a2c63..42407cd85 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-container.tsx @@ -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 ( @@ -35,7 +37,10 @@ export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => { error={error} > {data ? ( - + create({ orderSubmission })} + /> ) : (

{t('Could not load market')}

diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-manager.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-manager.tsx deleted file mode 100644 index 621200eb5..000000000 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-manager.tsx +++ /dev/null @@ -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 ( -
    -
  • - {t('The connection to your Vega Wallet has been lost.')} -
  • -
  • - -
  • -
- ); - } - return ( -

- {error.message}{' '} - {error instanceof WalletError ? `: ${error.data}` : null} -

- ); - } - return null; - }, [transaction, reconnect]); -}; - -export const DealTicketManager = ({ - market, - children, -}: DealTicketManagerProps) => { - const { submit, transaction, finalizedOrder, Dialog, reset } = - useOrderSubmit(); - return ( - <> - {children || ( - submit(order)} - transactionStatus={ - transaction.status === VegaTxStatus.Requested || - transaction.status === VegaTxStatus.Pending - ? 'pending' - : 'default' - } - /> - )} - - ), - Error: , - }} - /> - - ); -}; - -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 ; - case Schema.OrderStatus.STATUS_REJECTED: - case Schema.OrderStatus.STATUS_STOPPED: - case Schema.OrderStatus.STATUS_CANCELLED: - return ; - default: - return; - } -}; diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx index 4c5d19156..8170b3fe8 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.spec.tsx @@ -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 ( - + ); diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx index 1ac186514..76e3d678d 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -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 = ({ )} = 1} - transactionStatus={transactionStatus} variant={order.side === Schema.Side.SIDE_BUY ? 'ternary' : 'secondary'} /> { 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 ( (isOpen ? open() : close())} {...dialogStyleProps} > diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index 8ed50201b..922049059 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -322,7 +322,6 @@ const FormButton = ({ ); } else if (chainId !== desiredChainId) { - console.log(chainId, desiredChainId); const chainName = getChainName(desiredChainId); message = t(`This app only works on ${chainName}.`); button = ( diff --git a/libs/deposits/src/lib/deposit-manager.tsx b/libs/deposits/src/lib/deposit-manager.tsx index c37a52fd0..6a3fde583 100644 --- a/libs/deposits/src/lib/deposit-manager.tsx +++ b/libs/deposits/src/lib/deposit-manager.tsx @@ -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['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 && ( 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 = ({ - ); }; diff --git a/libs/deposits/src/lib/deposit-store.ts b/libs/deposits/src/lib/deposit-store.ts deleted file mode 100644 index b9e0d13f3..000000000 --- a/libs/deposits/src/lib/deposit-store.ts +++ /dev/null @@ -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) => void; -} - -export const useDepositStore = create((set) => ({ - balance: new BigNumber(0), - allowance: new BigNumber(0), - deposited: new BigNumber(0), - max: new BigNumber(0), - asset: undefined, - update: (updatedState) => { - set(updatedState); - }, -})); diff --git a/libs/deposits/src/lib/index.ts b/libs/deposits/src/lib/index.ts index 299f49921..7155f2912 100644 --- a/libs/deposits/src/lib/index.ts +++ b/libs/deposits/src/lib/index.ts @@ -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'; diff --git a/libs/deposits/src/lib/use-deposit-balances.ts b/libs/deposits/src/lib/use-deposit-balances.ts index 05b89ee68..4dd2c5b19 100644 --- a/libs/deposits/src/lib/use-deposit-balances.ts +++ b/libs/deposits/src/lib/use-deposit-balances.ts @@ -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; + +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(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 }; }; diff --git a/libs/deposits/src/lib/use-submit-approval.ts b/libs/deposits/src/lib/use-submit-approval.ts index e17c29aaa..bbaeb9624 100644 --- a/libs/deposits/src/lib/use-submit-approval.ts +++ b/libs/deposits/src/lib/use-submit-approval.ts @@ -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( 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); } diff --git a/libs/deposits/src/lib/use-submit-deposit.tsx b/libs/deposits/src/lib/use-submit-deposit.tsx deleted file mode 100644 index 60367620b..000000000 --- a/libs/deposits/src/lib/use-submit-deposit.tsx +++ /dev/null @@ -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(null); - - const getBalance = useGetBalanceOfERC20Token(tokenContract, asset); - - const transaction = useEthereumTransaction( - bridgeContract, - 'deposit_asset', - config?.confirmations, - true - ); - - useSubscription( - 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); - } - }, - }; -}; diff --git a/libs/deposits/src/lib/use-submit-faucet.ts b/libs/deposits/src/lib/use-submit-faucet.ts index 21bfbd86f..0da95cb72 100644 --- a/libs/deposits/src/lib/use-submit-faucet.ts +++ b/libs/deposits/src/lib/use-submit-faucet.ts @@ -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( 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); } diff --git a/libs/governance/src/lib/proposals-hooks/use-update-network-paramaters-toasts.tsx b/libs/governance/src/lib/proposals-hooks/use-update-network-paramaters-toasts.tsx index 220d7dd96..824a67319 100644 --- a/libs/governance/src/lib/proposals-hooks/use-update-network-paramaters-toasts.tsx +++ b/libs/governance/src/lib/proposals-hooks/use-update-network-paramaters-toasts.tsx @@ -65,9 +65,7 @@ export const useUpdateNetworkParametersToasts = (): Toast[] => { return { id: `update-network-param-proposal-${proposal.id}`, intent: Intent.Warning, - render: () => ( - - ), + content: , onClose: () => remove(id), closeAfter: CLOSE_AFTER, }; diff --git a/libs/orders/src/lib/components/order-data-provider/Orders.graphql b/libs/orders/src/lib/components/order-data-provider/Orders.graphql index 4b0f915fb..7a0f5130b 100644 --- a/libs/orders/src/lib/components/order-data-provider/Orders.graphql +++ b/libs/orders/src/lib/components/order-data-provider/Orders.graphql @@ -22,6 +22,12 @@ fragment OrderFields on Order { } } +query OrderById($orderId: ID!) { + orderByID(id: $orderId) { + ...OrderFields + } +} + query Orders( $partyId: ID! $pagination: Pagination diff --git a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts index 010b259c1..878b23d48 100644 --- a/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts +++ b/libs/orders/src/lib/components/order-data-provider/__generated__/Orders.ts @@ -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; @@ -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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(OrderByIdDocument, options); + } +export function useOrderByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(OrderByIdDocument, options); + } +export type OrderByIdQueryHookResult = ReturnType; +export type OrderByIdLazyQueryHookResult = ReturnType; +export type OrderByIdQueryResult = Apollo.QueryResult; export const OrdersDocument = gql` query Orders($partyId: ID!, $pagination: Pagination, $dateRange: DateRange, $filter: OrderFilter, $marketId: ID) { party(id: $partyId) { diff --git a/libs/orders/src/lib/components/order-feedback/order-feedback.tsx b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx index a375ae5c7..58b3f6700 100644 --- a/libs/orders/src/lib/components/order-feedback/order-feedback.tsx +++ b/libs/orders/src/lib/components/order-feedback/order-feedback.tsx @@ -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( diff --git a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx index 435272766..4858b7f61 100644 --- a/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx +++ b/libs/orders/src/lib/components/order-list-manager/order-list-manager.tsx @@ -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(); const [filter, setFilter] = useState(); const [editOrder, setEditOrder] = useState(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 = ({
)} - - ) : ( - - ), - }} - /> - - ), - }} - /> + {editOrder && ( { + 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 }); }} /> )} diff --git a/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx b/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx index a7ade441d..8f8dbccb6 100644 --- a/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-submit.spec.tsx @@ -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(), }, }); }); diff --git a/libs/orders/src/lib/order-hooks/use-order-submit.tsx b/libs/orders/src/lib/order-hooks/use-order-submit.tsx index 64130494f..ed4e58114 100644 --- a/libs/orders/src/lib/order-hooks/use-order-submit.tsx +++ b/libs/orders/src/lib/order-hooks/use-order-submit.tsx @@ -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); diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index d5fc9c582..40ee9d37d 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -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(null); const { data, error, loading, getRows } = usePositionsData(partyId, gridRef); - const { - submit, - closingOrder, - closingOrderResult, - transaction, - transactionResult, - Dialog, - } = useClosePosition(); + const create = useVegaTransactionStore((store) => store.create); return (
@@ -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} />
@@ -41,63 +56,6 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => { noDataCondition={(data) => !(data && data.length)} />
- , - Complete: ( - - ), - }} - />
); }; - -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 ; - } - - return ; -}; - -const getDialogTitle = (transactionResult?: TransactionResult) => { - if (!transactionResult) { - return; - } - - if (transactionResult.status) { - return t('Position closed'); - } - - return t('Position not closed'); -}; diff --git a/libs/react-helpers/src/hooks/index.ts b/libs/react-helpers/src/hooks/index.ts index 4ddd89396..f425085a8 100644 --- a/libs/react-helpers/src/hooks/index.ts +++ b/libs/react-helpers/src/hooks/index.ts @@ -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'; diff --git a/libs/react-helpers/src/hooks/use-previous.spec.ts b/libs/react-helpers/src/hooks/use-previous.spec.ts new file mode 100644 index 000000000..fb5926918 --- /dev/null +++ b/libs/react-helpers/src/hooks/use-previous.spec.ts @@ -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'); + }); +}); diff --git a/apps/token/src/hooks/use-previous.ts b/libs/react-helpers/src/hooks/use-previous.ts similarity index 53% rename from apps/token/src/hooks/use-previous.ts rename to libs/react-helpers/src/hooks/use-previous.ts index da91455a2..0e15a36e3 100644 --- a/apps/token/src/hooks/use-previous.ts +++ b/libs/react-helpers/src/hooks/use-previous.ts @@ -1,9 +1,9 @@ -import React from 'react'; +import { useEffect, useRef } from 'react'; export function usePrevious(value: T): T | undefined { - const ref = React.useRef(value); + const ref = useRef(value); - React.useEffect(() => { + useEffect(() => { ref.current = value; }, [value]); diff --git a/libs/react-helpers/src/lib/grid/size.tsx b/libs/react-helpers/src/lib/grid/size.tsx index e9e57fefd..9245faa4f 100644 --- a/libs/react-helpers/src/lib/grid/size.tsx +++ b/libs/react-helpers/src/lib/grid/size.tsx @@ -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 ( {side === Schema.Side.SIDE_BUY diff --git a/libs/types/src/global-types-mappings.ts b/libs/types/src/global-types-mappings.ts index fd4b97fd6..3f408cf63 100644 --- a/libs/types/src/global-types-mappings.ts +++ b/libs/types/src/global-types-mappings.ts @@ -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', }; diff --git a/libs/ui-toolkit/src/components/toast/toast.stories.tsx b/libs/ui-toolkit/src/components/toast/toast.stories.tsx index 4ec06aa68..fa967458c 100644 --- a/libs/ui-toolkit/src/components/toast/toast.stories.tsx +++ b/libs/ui-toolkit/src/components/toast/toast.stories.tsx @@ -9,18 +9,14 @@ export default { } as ComponentMeta; const Template: ComponentStory = (args) => { - return ( - ( - <> -

Lorem ipsum dolor sit amet consectetur adipisicing elit.

-

Eaque exercitationem saepe cupiditate sunt impedit.

-

I really like 🥪🥪🥪!

- - )} - /> + const toastContent = ( + <> +

Lorem ipsum dolor sit amet consectetur adipisicing elit.

+

Eaque exercitationem saepe cupiditate sunt impedit.

+

I really like 🥪🥪🥪!

+ ); + return ; }; export const Default = Template.bind({}); diff --git a/libs/ui-toolkit/src/components/toast/toast.tsx b/libs/ui-toolkit/src/components/toast/toast.tsx index 8b10f2567..57e2455b1 100644 --- a/libs/ui-toolkit/src/components/toast/toast.tsx +++ b/libs/ui-toolkit/src/components/toast/toast.tsx @@ -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} diff --git a/libs/ui-toolkit/src/components/toast/toasts-container.spec.tsx b/libs/ui-toolkit/src/components/toast/toasts-container.spec.tsx index e9a093625..4cc9eb0fe 100644 --- a/libs/ui-toolkit/src/components/toast/toasts-container.spec.tsx +++ b/libs/ui-toolkit/src/components/toast/toasts-container.spec.tsx @@ -23,17 +23,17 @@ describe('ToastsContainer', () => { add({ id: 'toast-a', intent: Intent.None, - render: () =>

A

, + content:

A

, }); add({ id: 'toast-b', intent: Intent.None, - render: () =>

B

, + content:

B

, }); add({ id: 'toast-c', intent: Intent.None, - render: () =>

C

, + content:

C

, }); }); const { baseElement } = render( @@ -54,17 +54,17 @@ describe('ToastsContainer', () => { add({ id: 'toast-a', intent: Intent.None, - render: () =>

A

, + content:

A

, }); add({ id: 'toast-b', intent: Intent.None, - render: () =>

B

, + content:

B

, }); add({ id: 'toast-c', intent: Intent.None, - render: () =>

C

, + content:

C

, }); }); const { baseElement } = render( @@ -90,13 +90,13 @@ describe('ToastsContainer', () => { add({ id: 'toast-a', intent: Intent.None, - render: () =>

A

, + content:

A

, onClose: () => remove('toast-a'), }); add({ id: 'toast-b', intent: Intent.None, - render: () =>

B

, + content:

B

, onClose: () => remove('toast-b'), }); }); diff --git a/libs/ui-toolkit/src/components/toast/toasts-container.stories.tsx b/libs/ui-toolkit/src/components/toast/toasts-container.stories.tsx index 02e2827a8..f2c4ce33a 100644 --- a/libs/ui-toolkit/src/components/toast/toasts-container.stories.tsx +++ b/libs/ui-toolkit/src/components/toast/toasts-container.stories.tsx @@ -69,7 +69,7 @@ const randomToast = (): Toast => { Intent.Danger, Intent.Success, ]) as Intent, - render: () =>

{content}

, + content:

{content}

, closeAfter: sample([undefined, random(1000, 5000)]), }; }; @@ -114,20 +114,20 @@ const Template: ComponentStory = (args) => { ]; add({ ...t, - render: ({ id }) => ( + content: ( <>

{words[0]}