diff --git a/apps/console-lite-e2e/src/support/mocks/generate-party-market-data.ts b/apps/console-lite-e2e/src/support/mocks/generate-party-market-data.ts index 1a7bff715..71f7862a9 100644 --- a/apps/console-lite-e2e/src/support/mocks/generate-party-market-data.ts +++ b/apps/console-lite-e2e/src/support/mocks/generate-party-market-data.ts @@ -1,15 +1,29 @@ +import { Schema as Types } from '@vegaprotocol/types'; + export const generatePartyMarketData = () => { return { party: { id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1', accounts: [ { - type: 'General', + type: Types.AccountType.ACCOUNT_TYPE_GENERAL, balance: '1200000', asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' }, market: null, __typename: 'AccountBalance', }, + { + __typename: 'AccountBalance', + type: Types.AccountType.ACCOUNT_TYPE_GENERAL, + balance: '0.000000001', + asset: { + __typename: 'Asset', + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + symbol: 'tUSD', + name: 'usd', + decimals: 0, + }, + }, ], marginsConnection: { edges: null, __typename: 'MarginConnection' }, positionsConnection: { edges: null, __typename: 'PositionConnection' }, diff --git a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx index e0b874834..78ecf2253 100644 --- a/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx +++ b/apps/console-lite/src/app/components/deal-ticket/deal-ticket-steps.tsx @@ -68,19 +68,20 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => { const emptyString = ' - '; const step = toDecimal(market.positionDecimalPlaces); const order = watch(); - const { message: invalidText, isDisabled } = useOrderValidation({ - market, - orderType: order.type, - orderTimeInForce: order.timeInForce, - fieldErrors: errors, - }); - const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit(); const { pubKey } = useVegaWallet(); const estMargin = useOrderMargin({ order, market, partyId: pubKey || '', }); + const { message: invalidText, isDisabled } = useOrderValidation({ + market, + orderType: order.type, + orderTimeInForce: order.timeInForce, + fieldErrors: errors, + estMargin, + }); + const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit(); const { data: partyBalance } = usePartyBalanceQuery({ variables: { partyId: pubKey || '' }, diff --git a/apps/static/src/assets/mainnet-tranches.json b/apps/static/src/assets/mainnet-tranches.json index e1e4e1c4e..c42618446 100644 --- a/apps/static/src/assets/mainnet-tranches.json +++ b/apps/static/src/assets/mainnet-tranches.json @@ -274,7 +274,7 @@ "tranche_end": "2023-12-05T00:00:00.000Z", "total_added": "129999.45", "total_removed": "0", - "locked_amount": "95719.310664575736600945", + "locked_amount": "95541.63881894388350853", "deposits": [ { "amount": "129999.45", @@ -340,7 +340,7 @@ "tranche_end": "2023-09-03T00:00:00.000Z", "total_added": "52600", "total_removed": "0", - "locked_amount": "44745.36689497716662", + "locked_amount": "44637.43481735159618", "deposits": [ { "amount": "2600", @@ -513,7 +513,7 @@ "tranche_end": "2023-09-17T00:00:00.000Z", "total_added": "5000", "total_removed": "0", - "locked_amount": "4445.142694063927", + "locked_amount": "4434.88299086758", "deposits": [ { "amount": "5000", @@ -724,7 +724,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "97499.58", "total_removed": "0", - "locked_amount": "37113.928727744633569794", + "locked_amount": "36939.6491980608633088128", "deposits": [ { "amount": "97499.58", @@ -757,7 +757,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "135173.4239508", "total_removed": "0", - "locked_amount": "50728.33311833605549421920164", + "locked_amount": "50490.123089363932785616473", "deposits": [ { "amount": "135173.4239508", @@ -790,7 +790,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "32499.86", "total_removed": "0", - "locked_amount": "15613.1888924949780453896", + "locked_amount": "15539.872504003513808234", "deposits": [ { "amount": "32499.86", @@ -823,7 +823,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "10833.29", "total_removed": "0", - "locked_amount": "5081.9414777083332611114", + "locked_amount": "5058.077704699755050653", "deposits": [ { "amount": "10833.29", @@ -856,7 +856,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "22749.93", "total_removed": "0", - "locked_amount": "18997.43227480366472295", + "locked_amount": "18908.224161420157492887", "deposits": [ { "amount": "6500", @@ -931,7 +931,7 @@ "tranche_end": "2022-11-01T00:00:00.000Z", "total_added": "30000", "total_removed": "0", - "locked_amount": "732.94836956521836", + "locked_amount": "610.8355978260882", "deposits": [ { "amount": "7500", @@ -1048,7 +1048,7 @@ "tranche_end": "2023-06-02T00:00:00.000Z", "total_added": "1939928.38", "total_removed": "626880.6921411330574", - "locked_amount": "1155960.359757785506826668", + "locked_amount": "1151979.741877591455301096", "deposits": [ { "amount": "1852091.69", @@ -1345,7 +1345,7 @@ "tranche_end": "2023-02-01T00:00:00.000Z", "total_added": "42500", "total_removed": "0", - "locked_amount": "22288.3435235507255", + "locked_amount": "22115.350430253619", "deposits": [ { "amount": "12500", @@ -6070,7 +6070,7 @@ "tranche_start": "2021-09-03T00:00:00.000Z", "tranche_end": "2022-09-03T00:00:00.000Z", "total_added": "40726.000000000000000003", - "total_removed": "25366.75324099301", + "total_removed": "25378.75324099301", "locked_amount": "0", "deposits": [ { @@ -12835,6 +12835,11 @@ "user": "0x38249D7DcC4eF7dFCa6579C069ADd3a1a4E4DeA3", "tx": "0x77a11d777ff82ec042d23eaa2b57c27c2980b73f8c2d59e143bd366be803473d" }, + { + "amount": "12", + "user": "0x54e6627177165Aa7b6F31752c744c306FaC993dB", + "tx": "0xb6efb136c6521cd37238227a9a8f47c1b25785c675d69fcb529a275624c4360b" + }, { "amount": "3.86333155832", "user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2", @@ -20311,10 +20316,17 @@ "tx": "0x846ec1e97e805281e019842be187b58f63237c0ea04cc2abd002899e86762b39" } ], - "withdrawals": [], + "withdrawals": [ + { + "amount": "12", + "user": "0x54e6627177165Aa7b6F31752c744c306FaC993dB", + "tranche_id": 11, + "tx": "0xb6efb136c6521cd37238227a9a8f47c1b25785c675d69fcb529a275624c4360b" + } + ], "total_tokens": "12", - "withdrawn_tokens": "0", - "remaining_tokens": "12" + "withdrawn_tokens": "12", + "remaining_tokens": "0" }, { "address": "0xcd6f7FBa16933469bd99F714CC802849216C532A", @@ -24896,7 +24908,7 @@ "tranche_end": "2023-06-05T00:00:00.000Z", "total_added": "3732368.4671", "total_removed": "223655.6603333956257", - "locked_amount": "1800809.935025694503261770146", + "locked_amount": "1794693.11124778421066616842", "deposits": [ { "amount": "1998.95815", @@ -25878,8 +25890,8 @@ "tranche_start": "2022-06-05T00:00:00.000Z", "tranche_end": "2023-12-05T00:00:00.000Z", "total_added": "15870102.715470999700000001", - "total_removed": "291604.2567888250208544", - "locked_amount": "11685243.9921930101885892916035514748661801", + "total_removed": "292058.2136868049826044", + "locked_amount": "11663554.1278141623085528184746161725790754", "deposits": [ { "amount": "16249.93", @@ -26428,6 +26440,11 @@ "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", "tx": "0x4843b5deac41256835df3ead02d423c04d869f007e99f8a3a1ee40fb47892ab6" }, + { + "amount": "453.95689797996175", + "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", + "tx": "0xa2f8867131a70c86f1a6ebe382bd3b6fa15acf5736e9a9634344695c58cbf071" + }, { "amount": "243.932422174205", "user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0", @@ -27286,6 +27303,12 @@ "tranche_id": 2, "tx": "0x4843b5deac41256835df3ead02d423c04d869f007e99f8a3a1ee40fb47892ab6" }, + { + "amount": "453.95689797996175", + "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", + "tranche_id": 2, + "tx": "0xa2f8867131a70c86f1a6ebe382bd3b6fa15acf5736e9a9634344695c58cbf071" + }, { "amount": "981.32183505687375", "user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b", @@ -27996,8 +28019,8 @@ } ], "total_tokens": "259998.8875", - "withdrawn_tokens": "68142.48983552186", - "remaining_tokens": "191856.39766447814" + "withdrawn_tokens": "68596.44673350182175", + "remaining_tokens": "191402.44076649817825" }, { "address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c", @@ -29645,8 +29668,8 @@ "tranche_start": "2021-11-05T00:00:00.000Z", "tranche_end": "2023-05-05T00:00:00.000Z", "total_added": "14597706.0446472999", - "total_removed": "3142112.555266197783780637", - "locked_amount": "5066297.416314942287083535753197734", + "total_removed": "3142743.615739270345670137", + "locked_amount": "5046273.47201148262856834879523159", "deposits": [ { "amount": "129284.449", @@ -29880,6 +29903,11 @@ "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", "tx": "0x662f17a2fb66cfceca6f93bd9265c2def6bddb15893a38c5383e14307c65f9d0" }, + { + "amount": "631.0604730725618895", + "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", + "tx": "0x6fa8423fdeb8ed0784552a008fd9e9d40b59f76622a559aafdfc5d8401ad2272" + }, { "amount": "1360.2331260920135955", "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", @@ -32216,6 +32244,12 @@ "tranche_id": 3, "tx": "0x662f17a2fb66cfceca6f93bd9265c2def6bddb15893a38c5383e14307c65f9d0" }, + { + "amount": "631.0604730725618895", + "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", + "tranche_id": 3, + "tx": "0x6fa8423fdeb8ed0784552a008fd9e9d40b59f76622a559aafdfc5d8401ad2272" + }, { "amount": "1360.2331260920135955", "user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b", @@ -34204,8 +34238,8 @@ } ], "total_tokens": "359123.469575", - "withdrawn_tokens": "233906.009470016341224", - "remaining_tokens": "125217.460104983658776" + "withdrawn_tokens": "234537.0699430889031135", + "remaining_tokens": "124586.3996319110968865" }, { "address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB", @@ -35323,7 +35357,7 @@ "tranche_end": "2023-04-05T00:00:00.000Z", "total_added": "5778205.3912159303", "total_removed": "2110079.858212660363479132", - "locked_amount": "1684821.346357521854252214981942818", + "locked_amount": "1676909.76485941822495456555758168", "deposits": [ { "amount": "552496.6455", @@ -36941,8 +36975,8 @@ "tranche_start": "2022-06-05T00:00:00.000Z", "tranche_end": "2023-06-05T00:00:00.000Z", "total_added": "472355.6199999996", - "total_removed": "6204.966976106296", - "locked_amount": "285348.62807326458926927321461188", + "total_removed": "6224.151146578296", + "locked_amount": "284379.38238039929630602147031964", "deposits": [ { "amount": "3000", @@ -43601,6 +43635,11 @@ "user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A", "tx": "0x241bc09d2972d7ea7c2eb18752e10305191bb94911837b64a860771a7890c014" }, + { + "amount": "19.184170472", + "user": "0x4F4A140E9B4a8403792970C6b5535ed0AA32c8A4", + "tx": "0x640d6905e3f009917c26c72a2ca24fc1cd17affc1706d75c0f1354cd6689ea18" + }, { "amount": "95.248756976", "user": "0xA17b91BdDcE741528fDDF8E5738A767B3053ac18", @@ -58620,6 +58659,12 @@ } ], "withdrawals": [ + { + "amount": "19.184170472", + "user": "0x4F4A140E9B4a8403792970C6b5535ed0AA32c8A4", + "tranche_id": 5, + "tx": "0x640d6905e3f009917c26c72a2ca24fc1cd17affc1706d75c0f1354cd6689ea18" + }, { "amount": "60.229674022", "user": "0x4F4A140E9B4a8403792970C6b5535ed0AA32c8A4", @@ -58628,8 +58673,8 @@ } ], "total_tokens": "200", - "withdrawn_tokens": "60.229674022", - "remaining_tokens": "139.770325978" + "withdrawn_tokens": "79.413844494", + "remaining_tokens": "120.586155506" }, { "address": "0x4DA5acEc788beF3d7F67cEA1c3CfeE1a41c1A21A", @@ -63920,7 +63965,7 @@ "tranche_start": "2021-12-05T00:00:00.000Z", "tranche_end": "2022-06-05T00:00:00.000Z", "total_added": "171288.42", - "total_removed": "56344.8822826097989", + "total_removed": "56554.8822826097989", "locked_amount": "0", "deposits": [ { @@ -68165,6 +68210,11 @@ "user": "0xbdaa56463cb068710Fc09A3481AdE4FbbAD0C46C", "tx": "0x13d47be74806d8c713d741517f59d91198bc962b4e3c7083dea0574cda6178b9" }, + { + "amount": "210", + "user": "0xB0F161160E2766f6D042a47521e5b95A249f2293", + "tx": "0xb45c2382abb70681e3fb129fd4c52d8b142171445222cdd0977543f1eaa2bc91" + }, { "amount": "40", "user": "0x7cE6eB9eCcbe0E2d35Cd4515d58b813d943BF2Ec", @@ -76161,10 +76211,17 @@ "tx": "0x057f65938b1360b6d2c4ebf5e789c67897cf60cb6a13f947004def03891afbd8" } ], - "withdrawals": [], + "withdrawals": [ + { + "amount": "210", + "user": "0xB0F161160E2766f6D042a47521e5b95A249f2293", + "tranche_id": 6, + "tx": "0xb45c2382abb70681e3fb129fd4c52d8b142171445222cdd0977543f1eaa2bc91" + } + ], "total_tokens": "210", - "withdrawn_tokens": "0", - "remaining_tokens": "210" + "withdrawn_tokens": "210", + "remaining_tokens": "0" }, { "address": "0x82b6405BCf9F4C8579af1FaB37619C849A75EFAA", diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts index ec000f713..48a09de48 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket.cy.ts @@ -3,7 +3,9 @@ import { MarketTradingMode, AuctionTrigger, } from '@vegaprotocol/types'; +import { generateEstimateOrder } from '../support/mocks/generate-fees'; import { connectVegaWallet } from '../support/vega-wallet'; +import { aliasQuery } from '@vegaprotocol/cypress'; const orderSizeField = 'order-size'; const orderPriceField = 'order-price'; @@ -201,7 +203,7 @@ describe('must submit order', { tags: '@smoke' }, () => { }); describe('deal ticket validation', { tags: '@smoke' }, () => { - before(() => { + beforeEach(() => { cy.mockTradingPage(); cy.visit('/markets/market-0'); cy.wait('@Market'); @@ -441,3 +443,40 @@ describe('suspended market validation', { tags: '@regression' }, () => { ); }); }); + +describe('margin required validation', { tags: '@regression' }, () => { + before(() => { + cy.mockTradingPage(); + cy.mockGQL((req) => { + aliasQuery( + req, + 'EstimateOrder', + generateEstimateOrder({ + estimateOrder: { + marginLevels: { __typename: 'MarginLevels', initialLevel: '1000' }, + }, + }) + ); + }); + cy.visit('/markets/market-0'); + connectVegaWallet(); + cy.wait('@Market'); + }); + + it('should display info and button for deposit', () => { + cy.getByTestId('place-order').should('be.disabled'); + cy.getByTestId('dealticket-error-message').should( + 'contain.text', + "You don't have enough margin available to open this position" + ); + cy.getByTestId('dealticket-error-message').should( + 'contain.text', + '0.01000 tBTC currently required, 0.00100 tBTC available' + ); + cy.getByTestId('deal-ticket-deposit-dialog-button').click(); + cy.getByTestId('dialog-content') + .find('h1') + .eq(0) + .should('have.text', 'Deposit'); + }); +}); diff --git a/apps/trading-e2e/src/support/mocks/generate-fees.ts b/apps/trading-e2e/src/support/mocks/generate-fees.ts new file mode 100644 index 000000000..6f68e6dd1 --- /dev/null +++ b/apps/trading-e2e/src/support/mocks/generate-fees.ts @@ -0,0 +1,98 @@ +import type { PartialDeep } from 'type-fest'; +import merge from 'lodash/merge'; +import { Schema as Types } from '@vegaprotocol/types'; +import type { + EstimateOrderQuery, + MarketMarkPriceQuery, + PartyBalanceQuery, + PartyMarketDataQuery, +} from '@vegaprotocol/deal-ticket'; + +const estimateOrderMock: EstimateOrderQuery = { + estimateOrder: { + __typename: 'OrderEstimate', + totalFeeAmount: '0.0006', + fee: { + __typename: 'TradeFee', + makerFee: '0.0001', + infrastructureFee: '0.0002', + liquidityFee: '0.0003', + }, + marginLevels: { __typename: 'MarginLevels', initialLevel: '1' }, + }, +}; + +export const generateEstimateOrder = ( + override?: PartialDeep +) => { + return merge(estimateOrderMock, override); +}; + +const marketMarkPriceMock: MarketMarkPriceQuery = { + market: { + __typename: 'Market', + decimalPlaces: 5, + data: { + __typename: 'MarketData', + markPrice: '100', + market: { __typename: 'Market', id: 'market-0' }, + }, + }, +}; + +export const generateMarkPrice = () => { + return marketMarkPriceMock; +}; + +const partyBalanceMock: PartyBalanceQuery = { + party: { + __typename: 'Party', + accounts: [ + { + __typename: 'AccountBalance', + type: Types.AccountType.ACCOUNT_TYPE_GENERAL, + balance: '100', + asset: { + __typename: 'Asset', + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + symbol: 'tBTC', + name: 'BTC', + decimals: 5, + }, + }, + ], + }, +}; + +export const generatePartyBalance = () => { + return partyBalanceMock; +}; + +export const generatePartyMarketData = (): PartyMarketDataQuery => { + return { + party: { + id: Cypress.env('VEGA_PUBLIC_KEY'), + accounts: [ + { + type: Types.AccountType.ACCOUNT_TYPE_GENERAL, + balance: '1200000', + asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' }, + market: null, + __typename: 'AccountBalance', + }, + { + __typename: 'AccountBalance', + type: Types.AccountType.ACCOUNT_TYPE_GENERAL, + balance: '100', + asset: { + __typename: 'Asset', + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + decimals: 5, + }, + }, + ], + marginsConnection: { edges: null, __typename: 'MarginConnection' }, + __typename: 'Party', + }, + }; +}; diff --git a/apps/trading-e2e/src/support/trading.ts b/apps/trading-e2e/src/support/trading.ts index 8fe700357..cc69b1e42 100644 --- a/apps/trading-e2e/src/support/trading.ts +++ b/apps/trading-e2e/src/support/trading.ts @@ -22,6 +22,12 @@ import { generateMargins, generatePositions } from './mocks/generate-positions'; import { generateStatistics } from './mocks/generate-statistics'; import { generateTrades } from './mocks/generate-trades'; import { generateWithdrawals } from './mocks/generate-withdrawals'; +import { + generateEstimateOrder, + generateMarkPrice, + generatePartyBalance, + generatePartyMarketData, +} from './mocks/generate-fees'; const mockTradingPage = ( req: CyHttpMessages.IncomingHttpRequest, @@ -96,6 +102,11 @@ const mockTradingPage = ( aliasQuery(req, 'Candles', generateCandles()); aliasQuery(req, 'Withdrawals', generateWithdrawals()); aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters()); + aliasQuery(req, 'EstimateOrder', generateEstimateOrder()); + aliasQuery(req, 'MarketMarkPrice', generateMarkPrice()); + aliasQuery(req, 'PartyBalance', generatePartyBalance()); + aliasQuery(req, 'MarketPositions', generatePositions()); + aliasQuery(req, 'PartyMarketData', generatePartyMarketData()); }; declare global { diff --git a/apps/trading/lib/apollo-client.ts b/apps/trading/lib/apollo-client.ts index 8d9b67ce5..4e3a1a3d0 100644 --- a/apps/trading/lib/apollo-client.ts +++ b/apps/trading/lib/apollo-client.ts @@ -60,6 +60,12 @@ export function createClient(base?: string) { AccountUpdate: { keyFields: false, }, + Party: { + keyFields: false, + }, + Fees: { + keyFields: false, + }, }, }); diff --git a/apps/trading/pages/_app.page.tsx b/apps/trading/pages/_app.page.tsx index 0a8dc0915..c0f0f1952 100644 --- a/apps/trading/pages/_app.page.tsx +++ b/apps/trading/pages/_app.page.tsx @@ -41,6 +41,9 @@ function AppBody({ Component, pageProps }: AppProps) { const [theme, toggleTheme] = useThemeSwitcher(); return ( + + + <div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]"> <AppLoader> diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-margin-validation.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-margin-validation.tsx new file mode 100644 index 000000000..d51b9bfc5 --- /dev/null +++ b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-margin-validation.tsx @@ -0,0 +1,47 @@ +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { AccountType } from '@vegaprotocol/types'; +import { toBigNum } from '@vegaprotocol/react-helpers'; +import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket'; +import type { OrderMargin } from '../../hooks/use-order-margin'; +import { usePartyBalanceQuery, useSettlementAccount } from '../../hooks'; + +interface Props { + market: DealTicketMarketFragment; + estMargin: OrderMargin | null; +} + +export const useOrderMarginValidation = ({ market, estMargin }: Props) => { + const { pubKey } = useVegaWallet(); + + const { data: partyBalance } = usePartyBalanceQuery({ + variables: { partyId: pubKey || '' }, + skip: !pubKey, + fetchPolicy: 'no-cache', + }); + + const settlementAccount = useSettlementAccount( + market.tradableInstrument.instrument.product.settlementAsset.id, + partyBalance?.party?.accounts || [], + AccountType.ACCOUNT_TYPE_GENERAL + ); + const balance = settlementAccount + ? toBigNum( + settlementAccount.balance || 0, + settlementAccount.asset.decimals || 0 + ) + : toBigNum('0', 0); + const margin = toBigNum(estMargin?.margin || 0, 0); + const { id, symbol, decimals } = + market.tradableInstrument.instrument.product.settlementAsset; + if (balance.isZero() || balance.isLessThan(margin)) { + return { + balance: balance.toString(), + margin: margin.toString(), + id, + symbol, + decimals, + }; + } + + return false; +}; diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx index e7b5efb08..ae0d1236f 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.spec.tsx @@ -1,5 +1,7 @@ +import * as React from 'react'; import { renderHook } from '@testing-library/react'; import { useVegaWallet } from '@vegaprotocol/wallet'; +import { MockedProvider } from '@apollo/client/testing'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import { MarketState, @@ -8,10 +10,11 @@ import { Schema, } from '@vegaprotocol/types'; import type { ValidationProps } from './use-order-validation'; -import { marketTranslations } from './use-order-validation'; -import { useOrderValidation } from './use-order-validation'; +import { marketTranslations, useOrderValidation } from './use-order-validation'; import { ERROR_SIZE_DECIMAL } from './validate-size'; import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket'; +import * as OrderMarginValidation from './use-order-margin-validation'; +import { ValidateMargin } from './validate-margin'; jest.mock('@vegaprotocol/wallet'); @@ -73,6 +76,15 @@ const defaultOrder = { step: 0.1, orderType: Schema.OrderType.TYPE_MARKET, orderTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK, + estMargin: { + margin: '0,000001', + totalFees: '0,000006', + fees: { + makerFee: '0,000003', + liquidityFee: '0,000002', + infrastructureFee: '0,000001', + }, + }, }; const ERROR = { @@ -99,16 +111,29 @@ function setup( ) { const mockUseVegaWallet = useVegaWallet as jest.Mock; mockUseVegaWallet.mockReturnValue({ ...defaultWalletContext, context }); - return renderHook(() => useOrderValidation({ ...defaultOrder, ...props })); + return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }), { + wrapper: MockedProvider, + }); } describe('useOrderValidation', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('Returns empty string when given valid data', () => { + jest + .spyOn(OrderMarginValidation, 'useOrderMarginValidation') + .mockReturnValue(false); + const { result } = setup(); expect(result.current).toStrictEqual({ isDisabled: false, message: `` }); }); it('Returns an error message when no keypair found', () => { + jest + .spyOn(OrderMarginValidation, 'useOrderMarginValidation') + .mockReturnValue(false); const { result } = setup(defaultOrder, { pubKey: null }); expect(result.current).toStrictEqual({ isDisabled: false, message: `` }); }); @@ -239,4 +264,28 @@ describe('useOrderValidation', () => { message: ERROR.FIELD_PRICE_STEP_DECIMAL, }); }); + + it('Returns an error message when the estimated margin is higher than collateral', async () => { + const invalidatedMockValue = { + balance: '0000000,1', + margin: '000,1', + id: 'instrument-id', + symbol: 'asset-symbol', + decimals: 5, + }; + jest + .spyOn(OrderMarginValidation, 'useOrderMarginValidation') + .mockReturnValue(invalidatedMockValue); + const { result } = setup({}); + + expect(result.current.isDisabled).toBe(true); + + const testElement = <ValidateMargin {...invalidatedMockValue} />; + expect((result.current.message as React.ReactElement)?.props).toEqual( + testElement.props + ); + expect((result.current.message as React.ReactElement)?.type).toEqual( + testElement.type + ); + }); }); diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx index a84e2fbec..5fa59fa59 100644 --- a/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx +++ b/libs/deal-ticket/src/components/deal-ticket-validation/use-order-validation.tsx @@ -15,6 +15,9 @@ import { ERROR_SIZE_DECIMAL } from './validate-size'; import { MarketDataGrid } from '../trading-mode-tooltip'; import { compileGridData } from '../trading-mode-tooltip/compile-grid-data'; import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket'; +import { ValidateMargin } from './validate-margin'; +import type { OrderMargin } from '../../hooks/use-order-margin'; +import { useOrderMarginValidation } from './use-order-margin-validation'; export const isMarketInAuction = (market: DealTicketMarketFragment) => { return [ @@ -30,6 +33,7 @@ export type ValidationProps = { orderType: Schema.OrderType; orderTimeInForce: Schema.OrderTimeInForce; fieldErrors?: FieldErrors<OrderSubmissionBody['orderSubmission']>; + estMargin: OrderMargin | null; }; export const marketTranslations = (marketState: MarketState) => { @@ -46,6 +50,7 @@ export const useOrderValidation = ({ fieldErrors = {}, orderType, orderTimeInForce, + estMargin, }: ValidationProps): { message: React.ReactNode | string; isDisabled: boolean; @@ -53,6 +58,8 @@ export const useOrderValidation = ({ const { pubKey } = useVegaWallet(); const minSize = toDecimal(market.positionDecimalPlaces); + const isInvalidOrderMargin = useOrderMarginValidation({ market, estMargin }); + const { message, isDisabled } = useMemo(() => { if (!pubKey) { return { message: t('No public key selected'), isDisabled: true }; @@ -266,6 +273,13 @@ export const useOrderValidation = ({ }; } + if (isInvalidOrderMargin) { + return { + isDisabled: true, + message: <ValidateMargin {...isInvalidOrderMargin} />, + }; + } + if ( [ MarketTradingMode.TRADING_MODE_BATCH_AUCTION, @@ -291,6 +305,7 @@ export const useOrderValidation = ({ fieldErrors?.price?.type, orderType, orderTimeInForce, + isInvalidOrderMargin, ]); return { message, isDisabled }; diff --git a/libs/deal-ticket/src/components/deal-ticket-validation/validate-margin.tsx b/libs/deal-ticket/src/components/deal-ticket-validation/validate-margin.tsx new file mode 100644 index 000000000..a6d800d94 --- /dev/null +++ b/libs/deal-ticket/src/components/deal-ticket-validation/validate-margin.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { formatNumber, t } from '@vegaprotocol/react-helpers'; +import { Button, Dialog } from '@vegaprotocol/ui-toolkit'; +import { useState } from 'react'; +import { DepositContainer } from '@vegaprotocol/deposits'; + +interface Props { + margin: string; + symbol: string; + id: string; + balance: string; + decimals: number; +} + +export const ValidateMargin = ({ + margin, + symbol, + id, + balance, + decimals, +}: Props) => { + const [depositDialog, setDepositDialog] = useState(false); + return ( + <> + <div + className="flex flex-col center pb-3" + data-testid="deal-ticket-margin-invalidated" + > + <p> + {t("You don't have enough margin available to open this position.")} + </p> + <p> + {`${formatNumber(margin, decimals)} ${symbol} ${t( + 'currently required' + )}, ${formatNumber(balance, decimals)} ${symbol} ${t('available')}`} + </p> + <Button + className="center mt-2" + variant="default" + size="xs" + data-testid="deal-ticket-deposit-dialog-button" + onClick={() => setDepositDialog(true)} + > + {t('Deposit')} {symbol} + </Button> + </div> + <Dialog open={depositDialog} onChange={setDepositDialog}> + <h1 className="text-2xl mb-4">{t('Deposit')}</h1> + <DepositContainer assetId={id} /> + </Dialog> + </> + ); +}; 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 66f78a3c8..8b5afaa5a 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket.tsx @@ -52,11 +52,16 @@ export const DealTicket = ({ defaultValues: getDefaultOrder(market), }); const order = watch(); + + const feeDetails = useFeeDealTicketDetails(order, market); + const details = getFeeDetailsValues(feeDetails); + const { message, isDisabled: disabled } = useOrderValidation({ market, orderType: order.type, orderTimeInForce: order.timeInForce, fieldErrors: errors, + estMargin: feeDetails.estMargin, }); const isDisabled = transactionStatus === 'pending' || disabled; @@ -101,9 +106,6 @@ export const DealTicket = ({ } }, [marketPriceFormatted, order.type, setValue]); - const feeDetails = useFeeDealTicketDetails(order, market); - const details = getFeeDetailsValues(feeDetails); - return ( <form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate> <Controller diff --git a/libs/deal-ticket/src/setup-tests.ts b/libs/deal-ticket/src/setup-tests.ts index 7b0828bfa..068c53d36 100644 --- a/libs/deal-ticket/src/setup-tests.ts +++ b/libs/deal-ticket/src/setup-tests.ts @@ -1 +1,2 @@ import '@testing-library/jest-dom'; +import 'jest-canvas-mock'; diff --git a/libs/market-depth/src/lib/market-depth-provider.spec.ts b/libs/market-depth/src/lib/market-depth-provider.spec.ts index d10364df1..9ccc6e2c3 100644 --- a/libs/market-depth/src/lib/market-depth-provider.spec.ts +++ b/libs/market-depth/src/lib/market-depth-provider.spec.ts @@ -1,12 +1,13 @@ import { update } from './market-depth-provider'; -import { captureException } from '@sentry/react'; +const mockCaptureException = jest.fn(); const reload = jest.fn(); + jest.mock('@sentry/react', () => { const original = jest.requireActual('@sentry/react'); // Step 2. return { ...original, - captureException: jest.fn(), + captureException: () => mockCaptureException(), }; }); @@ -15,7 +16,10 @@ jest.mock('./orderbook-data', () => ({ })); describe('market depth provider update', () => { - it('omits overlapping updates', () => { + beforeEach(() => { + mockCaptureException.mockClear(); + }); + it('omits not matching market', () => { const data = { id: '1', depth: { @@ -31,7 +35,7 @@ describe('market depth provider update', () => { expect(updatedData).toBe(data); }); - it('omits not matching market', () => { + it('omits overlapping updates', () => { const data = { id: '1', depth: { @@ -52,6 +56,7 @@ describe('market depth provider update', () => { ]; expect(update(data, delta.slice(0, 1), reload)).toBe(data); expect(update(data, delta.slice(1, 2), reload)).toBe(data); + expect(mockCaptureException).toBeCalledTimes(2); }); it('restarts and captureException when there is gap in updates', () => { @@ -71,6 +76,6 @@ describe('market depth provider update', () => { const updatedData = update(data, delta, reload); expect(updatedData).toBe(data); expect(reload).toBeCalled(); - expect(captureException).toBeCalled(); + expect(mockCaptureException).toBeCalled(); }); }); diff --git a/libs/market-depth/src/lib/market-depth-provider.ts b/libs/market-depth/src/lib/market-depth-provider.ts index a459cb544..957fa2ddc 100644 --- a/libs/market-depth/src/lib/market-depth-provider.ts +++ b/libs/market-depth/src/lib/market-depth-provider.ts @@ -23,13 +23,18 @@ export const update: Update< if (delta.marketId !== data.id) { continue; } - if (Number(delta.sequenceNumber) <= Number(data.depth.sequenceNumber)) { + if (BigInt(delta.sequenceNumber) <= BigInt(data.depth.sequenceNumber)) { + captureException( + new Error( + `Sequence number from delta is lower or equal to last sequenceNumber for ${data.id}, ${delta.sequenceNumber} <= ${data.depth.sequenceNumber}, update skipped` + ) + ); return data; } if (delta.previousSequenceNumber !== data.depth.sequenceNumber) { captureException( new Error( - `Sequence number gap in marketsDepthUpdate for {data.id}, {sequenceNumbers[delta.marketId]} - {delta.previousSequenceNumber}` + `Sequence number gap between delta previousSequenceNumber and stored sequence number in marketsDepthUpdate for market ${data.id}; ${delta.previousSequenceNumber} !== ${data.depth.sequenceNumber}, data provider reload` ) ); reload(); diff --git a/libs/market-info/src/components/market-expires/market-expires.spec.tsx b/libs/market-info/src/components/market-expires/market-expires.spec.tsx index 48071033f..b06f7f397 100644 --- a/libs/market-info/src/components/market-expires/market-expires.spec.tsx +++ b/libs/market-info/src/components/market-expires/market-expires.spec.tsx @@ -1,5 +1,6 @@ -import React from 'react'; import { render, screen } from '@testing-library/react'; +import React from 'react'; + import { MarketExpires } from './market-expires'; describe('MarketExpires', () => { @@ -13,7 +14,7 @@ describe('MarketExpires', () => { 'settlement:20220525T1200', ]; render(<MarketExpires tags={tags} />); - expect(screen.getByText('May 25')).toBeInTheDocument(); + expect(screen.getByText('25/05/2022, 12:00:00')).toBeInTheDocument(); }); it('settlement-date:date', () => { @@ -23,7 +24,7 @@ describe('MarketExpires', () => { 'settlement-date:2022-04-25T1200', ]; render(<MarketExpires tags={tags} />); - expect(screen.getByText('Apr 25')).toBeInTheDocument(); + expect(screen.getByText('25/04/2022, 12:00:00')).toBeInTheDocument(); }); it('last one proper tag should matter', () => { @@ -33,7 +34,7 @@ describe('MarketExpires', () => { 'settlement-expiry-date:2022-03-25T12:00:00', ]; render(<MarketExpires tags={tags} />); - expect(screen.getByText('Mar 25')).toBeInTheDocument(); + expect(screen.getByText('25/03/2022, 12:00:00')).toBeInTheDocument(); }); it('when no proper tag nor date should be null', () => { diff --git a/libs/market-info/src/components/market-expires/market-expires.tsx b/libs/market-info/src/components/market-expires/market-expires.tsx index 1989b0217..ee4daa134 100644 --- a/libs/market-info/src/components/market-expires/market-expires.tsx +++ b/libs/market-info/src/components/market-expires/market-expires.tsx @@ -1,8 +1,7 @@ -import type { SingleMarketFieldsFragment } from '@vegaprotocol/market-list'; -import { getDateFormat, t } from '@vegaprotocol/react-helpers'; -import { format, isValid, parseISO } from 'date-fns'; +import { getDateTimeFormat, t } from '@vegaprotocol/react-helpers'; +import { isValid, parseISO } from 'date-fns'; -export const EXPIRE_DATE_FORMAT = 'MMM dd'; +import type { SingleMarketFieldsFragment } from '@vegaprotocol/market-list'; export const getMarketExpiryDate = ( tags?: ReadonlyArray<string> | null @@ -31,7 +30,7 @@ export const getMarketExpiryDateFormatted = ( ): string | null => { if (tags) { const dateFound = getMarketExpiryDate(tags); - return dateFound ? format(dateFound, EXPIRE_DATE_FORMAT) : null; + return dateFound ? getDateTimeFormat().format(dateFound) : null; } return null; }; @@ -45,14 +44,14 @@ export const getExpiryDate = (market: SingleMarketFieldsFragment): string => { let content = null; if (!closeDate) { content = closedMarketDate - ? `Expired on ${getDateFormat().format(closedMarketDate)}` + ? `Expired on ${getDateTimeFormat().format(closedMarketDate)}` : t('Not time-based'); } else { const isExpired = Date.now() - closeDate.valueOf() > 0; - const expiryDate = getDateFormat().format(closeDate); + const expiryDate = getDateTimeFormat().format(closeDate); if (isExpired) { content = closedMarketDate - ? `Expired on ${getDateFormat().format(closedMarketDate)}` + ? `Expired on ${getDateTimeFormat().format(closedMarketDate)}` : t('Expired'); } else { content = expiryDate; diff --git a/libs/smart-contracts/src/contracts/multisig-control.ts b/libs/smart-contracts/src/contracts/multisig-control.ts index 6826378eb..6f38fe797 100644 --- a/libs/smart-contracts/src/contracts/multisig-control.ts +++ b/libs/smart-contracts/src/contracts/multisig-control.ts @@ -1,4 +1,4 @@ -import { ethers } from 'ethers'; +import { ethers, BigNumber } from 'ethers'; import abi from '../abis/multisig_abi.json'; export class MultisigControl { @@ -14,11 +14,15 @@ export class MultisigControl { } add_signer(newSigner: string, nonce: string, signatures: string) { - return this.contract.add_signer(newSigner, nonce, signatures); + return this.contract.add_signer( + newSigner, + BigNumber.from(nonce), + signatures + ); } burn_nonce(nonce: string, signatures: string) { - return this.contract.burn_nonce(nonce, signatures); + return this.contract.burn_nonce(BigNumber.from(nonce), signatures); } get_current_threshold() { @@ -38,11 +42,19 @@ export class MultisigControl { } remove_signer(oldSigner: string, nonce: string, signatures: string) { - return this.contract.remove_signer(oldSigner, nonce, signatures); + return this.contract.remove_signer( + oldSigner, + BigNumber.from(nonce), + signatures + ); } set_threshold(newThreshold: string, nonce: string, signatures: string) { - return this.contract.set_threshold(newThreshold, nonce, signatures); + return this.contract.set_threshold( + newThreshold, + BigNumber.from(nonce), + signatures + ); } signers(address: string) { @@ -50,6 +62,10 @@ export class MultisigControl { } verify_signatures(nonce: string, message: string, signatures: string) { - return this.contract.verify_signatures(nonce, message, signatures); + return this.contract.verify_signatures( + BigNumber.from(nonce), + message, + signatures + ); } } diff --git a/libs/ui-toolkit/src/components/dialog/dialog.tsx b/libs/ui-toolkit/src/components/dialog/dialog.tsx index bf52a8e27..4288f55e8 100644 --- a/libs/ui-toolkit/src/components/dialog/dialog.tsx +++ b/libs/ui-toolkit/src/components/dialog/dialog.tsx @@ -29,7 +29,7 @@ export function Dialog({ size = 'small', }: DialogProps) { const contentClasses = classNames( - 'fixed top-0 left-0 z-20 flex justify-center items-start overflow-scroll', + 'fixed top-0 left-0 z-20 flex justify-center items-start overflow-auto', 'w-full h-full' ); const wrapperClasses = classNames(