diff --git a/.env.develop b/.env.develop index 284e864d..6432a635 100644 --- a/.env.develop +++ b/.env.develop @@ -1 +1 @@ -VITE_FEATURE_TOGGLE_CLIENT_KEY=*:development.7adfb132cb6c86bf5f9e5b35fe5cc72bccdd22b526caccd143007e81 \ No newline at end of file +VITE_FEATURE_TOGGLE_CLIENT_KEY=*:development.2a6735ae1458c0527bc04a3cb8b092e0156f311a09d6e8c54503987a \ No newline at end of file diff --git a/.github/workflows/deploy-to-cf-pages.yaml b/.github/workflows/deploy-to-cf-pages.yaml index b2323357..aeedc97a 100644 --- a/.github/workflows/deploy-to-cf-pages.yaml +++ b/.github/workflows/deploy-to-cf-pages.yaml @@ -24,6 +24,7 @@ jobs: timeout-minutes: 5 env: NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + BRANCH_NAME: ${{ github.head_ref }} steps: - name: "Checkout Github Action" uses: actions/checkout@v4 @@ -39,6 +40,7 @@ jobs: run: | touch .env echo VITE_FEATURE_TOGGLE_CLIENT_KEY=${{ vars.VITE_FEATURE_TOGGLE_CLIENT_KEY }} >> .env + echo VITE_BRANCH_NAME=${{ env.BRANCH_NAME }} >> .env - name: Configure npm authentication for GitHub Registry env: NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/vault-docker-helm.yaml b/.github/workflows/vault-docker-helm.yaml index 0a751d9e..fa26d30d 100644 --- a/.github/workflows/vault-docker-helm.yaml +++ b/.github/workflows/vault-docker-helm.yaml @@ -96,7 +96,7 @@ jobs: name: ${{ vars.DOCKER_IMAGE }} repository: charts tag: ${{ steps.meta.outputs.tags }} - path: deploy/chart + path: helm registry: ${{ env.DOCKER_REGISTRY }} registry_username: ${{ env.DOCKER_USERNAME }} registry_password: ${{ env.DOCKER_PASSWORD }} diff --git a/package.json b/package.json index 88107b7f..4cf0a327 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "pnpm": ">=9.7.0" }, "dependencies": { - "@compolabs/spark-orderbook-ts-sdk": "https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.11.tgz", + "@compolabs/spark-orderbook-ts-sdk": "https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.10.tgz", + "@compolabs/spark-perpetual-ts-sdk": "https://registry.npmjs.org/@compolabs/spark-perpetual-ts-sdk/-/spark-perpetual-ts-sdk-0.0.15.tgz", "@compolabs/tradingview-chart": "^1.0.21", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", @@ -38,7 +39,7 @@ "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.10", "framer-motion": "^11.0.3", - "fuels": "^0.96.1", + "fuels": "^0.97.1", "gh-pages": "^6.1.1", "lightweight-charts": "^4.2.1", "lodash": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1227b78..527caaa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,11 @@ importers: .: dependencies: '@compolabs/spark-orderbook-ts-sdk': - specifier: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.11.tgz - version: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.11.tgz(@types/react@18.3.13)(fuels@0.96.1)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.10.tgz + version: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.10.tgz(@types/react@18.3.13)(fuels@0.97.1)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@compolabs/spark-perpetual-ts-sdk': + specifier: https://registry.npmjs.org/@compolabs/spark-perpetual-ts-sdk/-/spark-perpetual-ts-sdk-0.0.15.tgz + version: https://registry.npmjs.org/@compolabs/spark-perpetual-ts-sdk/-/spark-perpetual-ts-sdk-0.0.15.tgz(@types/react@18.3.13)(bufferutil@4.0.8)(fuels@0.97.1)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10) '@compolabs/tradingview-chart': specifier: ^1.0.21 version: 1.0.21 @@ -22,10 +25,10 @@ importers: version: 11.13.5(@emotion/react@11.13.5(@types/react@18.3.13)(react@18.3.1))(@types/react@18.3.13)(react@18.3.1) '@fuels/connectors': specifier: ^0.36.1 - version: 0.36.1(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(@wagmi/connectors@5.0.26(@types/react@18.3.13)(@wagmi/core@2.12.2(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(react@18.3.1)(typescript@5.7.2)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.13)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.28.0)(typescript@5.7.2)(utf-8-validate@5.0.10)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1))(bufferutil@4.0.8)(fuels@0.96.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1) + version: 0.36.1(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(@wagmi/connectors@5.0.26(@types/react@18.3.13)(@wagmi/core@2.12.2(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(react@18.3.1)(typescript@5.7.2)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.13)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.28.0)(typescript@5.7.2)(utf-8-validate@5.0.10)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1))(bufferutil@4.0.8)(fuels@0.97.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1) '@fuels/react': specifier: ^0.36.1 - version: 0.36.1(@tanstack/react-query@5.62.2(react@18.3.1))(@types/react-dom@18.3.1)(@types/react@18.3.13)(fuels@0.96.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.36.1(@tanstack/react-query@5.62.2(react@18.3.1))(@types/react-dom@18.3.1)(@types/react@18.3.13)(fuels@0.97.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@intercom/messenger-js-sdk': specifier: ^0.0.14 version: 0.0.14 @@ -72,8 +75,8 @@ importers: specifier: ^11.0.3 version: 11.13.1(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fuels: - specifier: ^0.96.1 - version: 0.96.1 + specifier: ^0.97.1 + version: 0.97.1 gh-pages: specifier: ^6.1.1 version: 6.2.0 @@ -1000,13 +1003,20 @@ packages: '@coinbase/wallet-sdk@4.0.4': resolution: {integrity: sha512-74c040CRnGhfRjr3ArnkAgud86erIqdkPHNt5HR1k9u97uTIZCJww9eGYT67Qf7gHPpGS/xW8Be1D4dvRm63FA==} - '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.11.tgz': - resolution: {tarball: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.11.tgz} - version: 1.14.11 + '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.10.tgz': + resolution: {tarball: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.10.tgz} + version: 1.14.10 engines: {node: '>=18'} peerDependencies: fuels: '>=0.97.1' + '@compolabs/spark-perpetual-ts-sdk@https://registry.npmjs.org/@compolabs/spark-perpetual-ts-sdk/-/spark-perpetual-ts-sdk-0.0.15.tgz': + resolution: {tarball: https://registry.npmjs.org/@compolabs/spark-perpetual-ts-sdk/-/spark-perpetual-ts-sdk-0.0.15.tgz} + version: 0.0.15 + engines: {node: '>=20'} + peerDependencies: + fuels: '>=0.98.0' + '@compolabs/tradingview-chart@1.0.21': resolution: {integrity: sha512-efExlG9Ki5uqEe16nRF2A6FwQ8CnzmAwad2kMZxNFtNsbJTYhVC1SMSzkcBNQ50yzmijm3HAUUfaMzsXwtEkFQ==, tarball: https://npm.pkg.github.com/download/@compolabs/tradingview-chart/1.0.21/f9b8234ef67206216f98b5919ef16173c21f7fe9} @@ -1443,69 +1453,73 @@ packages: '@ethersproject/web@5.7.1': resolution: {integrity: sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==} - '@fuel-ts/abi-coder@0.96.1': - resolution: {integrity: sha512-czJxFPirhSO6ayshu9Cr5AmED/IprQ8h1igwlcSHFr5jR+l46bLdlsXsWiUwe7vyvZCpOnxkN/XZYkmQyNxKNg==} + '@fuel-ts/abi-coder@0.97.1': + resolution: {integrity: sha512-ZuBnx8hJi3BDs12tlnBrvF/G9ciHncIkekMjJYWUkM745fKCQfhud5d6RiP9PyqugKY3luXgF2uNBZshXLHN5w==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/abi-typegen@0.96.1': - resolution: {integrity: sha512-vRJnAQ9sxkKuUQ2fkxntPm/679grxgwSf54O7vgDeXuXqQx/4XeylpExjH0/nZzEM5al+muw4rg9WwmKulkcZA==} + '@fuel-ts/abi-typegen@0.97.1': + resolution: {integrity: sha512-aNW/WbWXTQfJYZn6BN4Hw2X77somkWZN6DlM9KbsLpzpx0tQU/FHtM+6LGQdiuWzzMzyehGNZv/M6hjuZ/jlUg==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} hasBin: true - '@fuel-ts/account@0.96.1': - resolution: {integrity: sha512-i9InTebg3/buKXNJZpKBhxGMBC3MKpk905Uiv2CvTmldyPVgnZV/jY/b63jlfdEWyP5yVkr5/tzo7QaPKpfnMA==} + '@fuel-ts/account@0.97.1': + resolution: {integrity: sha512-aOvLQ0oppuRug7P25o6GkcXjy0+l3quWQWruQGYEUW9PYg7LvRx4PGvGmcEObC8JeOHJ76U1DVPz3wzNqYVqPA==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/address@0.96.1': - resolution: {integrity: sha512-PCuC8oojoWLhh0+boitaP90/Z3iSfEXZR8v1JCEfoLZIkvCq6EGKtiY0KvcNkPozHOHmmT34w/1CS6qHOoCBNA==} + '@fuel-ts/address@0.97.1': + resolution: {integrity: sha512-QjDexGzawpDXsDqhMYE8jGzWlz9nvzJavxe3ItEdHLySEIDB30hWOabwQkeBf1urDUs3YjdnNK3GSjk6bWIhrw==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/contract@0.96.1': - resolution: {integrity: sha512-shwNMHFZ7vr5QJsfVQ6aLd1ms88f3ZagrsNLqNz1Txhz/5KVgYyJaxfp7tUc4WZFpR67+W1zeIqx8ZNHVhck3g==} + '@fuel-ts/contract@0.97.1': + resolution: {integrity: sha512-3oxE/ybqWEfY0P/JCqGlXBYCKHmDPJNk0ArtTDRe6tPHM2UyuNCj8RXxQUGSNh6HTGsIWKwIA0udmpLNZSd1ag==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/crypto@0.96.1': - resolution: {integrity: sha512-OrAZPZtm8HQouzip621/ci47PeTS06QegGdz7MeN6wK4yeCbJmlRQ1WE9S3iclb3p+VLF0RtbecEbHQfsNgsnA==} + '@fuel-ts/crypto@0.97.1': + resolution: {integrity: sha512-AWnjVzLdK03R+Zb/qDMMwPMkDIBj191Vm3H1zAxGM8hUEP3wH8FUQ3383pkvA9MgJZ/zgiOjQ43gGCnra0IP/Q==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/errors@0.96.1': - resolution: {integrity: sha512-Xtso5v4a3UUvnMaOSDhMRlkb9LxLyCyC/1/RY7fZZ035ttscy6dNMuiD/iSptJ5pHklJ5R4rCPdOL5EKpgOaMA==} + '@fuel-ts/errors@0.97.1': + resolution: {integrity: sha512-8eO4EV7k1Y4SsVRLjClTqCyxKB47psIo+qyVk6XObGMiOswjAjzapSdQfwZSlo59BGns3aNAA34V2SJiTu1fwg==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/hasher@0.96.1': - resolution: {integrity: sha512-7z4cah+5TOcCBA2Wgvje1L7wVTahiFLb9IUpRXRMVGXwaqsbV/wUNcyuc1mhPh/JKLgcOe+OqsrpcYD2dg2rpg==} + '@fuel-ts/hasher@0.97.1': + resolution: {integrity: sha512-8f3qU7SZs3GceYYyB6bIzLH4pnkfYg8PTnhjqfXYEJw6QXpGKXDIE1pMyGz/FceShrFNP1ixNO7v9tcrdZG5og==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/interfaces@0.96.1': - resolution: {integrity: sha512-mZ3sDHJml5AtLRSmGWo5rbU9//3oKDhAeiFealajcPaVztAAnaKnA5a19dd4ITbjZb2q3e2lQ7zX8iAXgHUwYA==} + '@fuel-ts/interfaces@0.97.1': + resolution: {integrity: sha512-zjzQPIUT9ZKSakwSbJ4A6mvnG+jrZgabTLCTL+7jiFQT5lBb60lhSdMXW5vbzVhWWvcTxaU8rej9CrjuXXCwkQ==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/math@0.96.1': - resolution: {integrity: sha512-AZUChguQmE1ILYbcc6SOTjFJIUkGzD3Z6yHgvO4tszn2QS/jVTSAZKVx653J5u9m/Xn/WthwR35l1GbwXMwB4g==} + '@fuel-ts/math@0.97.1': + resolution: {integrity: sha512-tnql9KnXrhRV7p/HUx52EUTO7VsR0m590b621HyB+6jonhLWWBu7yla538g40Zt+Oto/0yYnAyGMN93joIqcbQ==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/merkle@0.96.1': - resolution: {integrity: sha512-7cLbxYG5berKSK+wPKrkbB9dZ6akOm7C1Ij4iwYXOxQWlmXY62mjSY5Tl1PPyDBSdRqBtqxW0xYUGIzZl2A/3g==} + '@fuel-ts/merkle@0.97.1': + resolution: {integrity: sha512-rqiSSb4AUk9yf8FCzVniW5fiCkBliRnQDsLi+A8/E5gFuCc/bTN7Ws1J5ERTOMSly7ScCRRTUgYt+BSf7wzl2A==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/program@0.96.1': - resolution: {integrity: sha512-tA2YvcIdlDQLKeOOfoTyfI3LRQZiqsHi5rFaGbOsjEQvyNQBdXa92vMMFW0dFNWXqOPLjTmYNWqp0xsMuikbsA==} + '@fuel-ts/program@0.97.1': + resolution: {integrity: sha512-y1AT8H2jFVTnnfObQqtmb38xLpQn7KRZ7lsAUJZk1Pe2P61dolv6e0sGax7dOjC8uzOFq+ygwEhDZzXqZ1WJOQ==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/script@0.96.1': - resolution: {integrity: sha512-N/N3I3xjDI1+9XcCrYTWapF0iBbdvtBxUUUZxn5S5GuIXuTCK6UfQLrRciuCHK5QiHVwFmQjlki+ozJfI+4UbQ==} + '@fuel-ts/recipes@0.97.1': + resolution: {integrity: sha512-dQB29TZ4H1BOM+gfahojR5Vgcblj7HkaUrfFqhHZ4tImrwOVz/vb0cmz/8wSDT2bk017sDV1D59BB3BA752hUw==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/transactions@0.96.1': - resolution: {integrity: sha512-yPQBzMeFIzNBLUZigaf4q0hETO8AH8Evt6ZvpWrYhjYz2WfEpfNDpuhIHRwsMfJRbxz2vhQ51AzkRqpH0b2GMQ==} + '@fuel-ts/script@0.97.1': + resolution: {integrity: sha512-B86EBGNL8TL5TE8KmOcKb4LoA2v2XcQMsk0VEBuFTMyLLrOE+Yf96N509lsIhu2p1d/mBDbIf8yb/N1KZ7Zr6Q==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/utils@0.96.1': - resolution: {integrity: sha512-XXZNEUPf7qtKpVO3ak2CSroBYEKh1Gne1zlmVSNUoVPqQvglcu0I2pu/QmVnZBN4m3yVSyHUoWR2Kbo8/Dh7sA==} + '@fuel-ts/transactions@0.97.1': + resolution: {integrity: sha512-aehrq8clj3MBo+ssg9QEesrNdbzmxiWSo8YqWU/cf7D+331CjRo5PIht3evsp9rDWPFtK5h7e3NzJzaV2wopDA==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} - '@fuel-ts/versions@0.96.1': - resolution: {integrity: sha512-C//ZT7U68Gksz9PzUJVzdzUARK7mfXf3MsF5ZqZaMegkE2tCy+twjy8PBdeVserY0uX6mEHRJyZJwcspk34ixg==} + '@fuel-ts/utils@0.97.1': + resolution: {integrity: sha512-RTm55rmfY55DI8P8XOQq5sEvRfE5XT1K15lLzRjSNvEB6QxO6Mv2D7pSBOcmczZIdcajLvBII40tAaaGxY16ww==} + engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} + + '@fuel-ts/versions@0.97.1': + resolution: {integrity: sha512-rKHBLolZpsaTEFHMshDmMT8BxwQnukOHPCMbb91QXD9qBBbOffCyMIfkljY8teBtxcvgWkF9d96GO+V9UAaL6w==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} hasBin: true @@ -1521,8 +1535,8 @@ packages: fuels: '>=0.96.1' react: '>=18.0.0' - '@fuels/vm-asm@0.58.0': - resolution: {integrity: sha512-tfarairW3IAtyoAIL3I5EJiUQzKAsY4J+eLgZg58B7+itDxqF+CUEpKanmiUnt1mBgry5GwtZsPIrUJ7OgTcDA==} + '@fuels/vm-asm@0.58.2': + resolution: {integrity: sha512-1/5azTzKJP508BXbZvM6Y0V5bCCX5JgEnd/8mXdBFmFvNLOhiYbwb25yk26auqOokfBXvthSkdkrvipEFft6jQ==} '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} @@ -1877,6 +1891,16 @@ packages: '@pythnetwork/hermes-client@1.3.1': resolution: {integrity: sha512-iJq4Surv9TKEwMIdhSnOxSdBSCfyUR+J4MPOvoFm2EisQBGBhDHIwDM4wPfbd/hBUiUcvVQoWWqeiSlznJ3iPQ==} + '@pythnetwork/price-service-client@1.9.0': + resolution: {integrity: sha512-SLm3IFcfmy9iMqHeT4Ih6qMNZhJEefY14T9yTlpsH2D/FE5+BaGGnfcexUifVlfH6M7mwRC4hEFdNvZ6ebZjJg==} + deprecated: This package is deprecated and is no longer maintained. Please use @pythnetwork/hermes-client instead. + + '@pythnetwork/price-service-sdk@1.7.1': + resolution: {integrity: sha512-xr2boVXTyv1KUt/c6llUTfbv2jpud99pWlMJbFaHGUBoygQsByuy7WbjIJKZ+0Blg1itLZl0Lp/pJGGg8SdJoQ==} + + '@pythnetwork/pyth-evm-js@1.82.0': + resolution: {integrity: sha512-NoKjAgElQEE+uEtAj08zlRJehFmcZ4VQStgehyCQjTs0pZ6amj7TJaD+qyIfa04ObZ/qlaQwzBoT5cGvZTmKow==} + '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} @@ -3249,6 +3273,9 @@ packages: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} + axios-retry@3.9.1: + resolution: {integrity: sha512-8PJDLJv7qTTMMwdnbMvrLYuvB47M81wRtxQmEdV5w4rgbTXTt+vtPkXwajOfOdSyv/wZICJOC+/UhXH4aQ/R+w==} + axios@1.7.9: resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} @@ -4252,10 +4279,6 @@ packages: resolution: {integrity: sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==} engines: {node: '>=12.0.0'} - extract-files@9.0.0: - resolution: {integrity: sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==} - engines: {node: ^10.17.0 || ^12.0.0 || >= 13.7.0} - eyes@0.1.8: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} @@ -4381,10 +4404,6 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - form-data@3.0.2: - resolution: {integrity: sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==} - engines: {node: '>= 6'} - form-data@4.0.1: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} @@ -4419,8 +4438,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fuels@0.96.1: - resolution: {integrity: sha512-BquRIJ0qHNKwhqBTNa6X4aghqhCj1Qz0jM+P0bziXAEk7gVJJZWRz10jCcAgZVeEb4AMdZLDsnNzKJ51GH8IvQ==} + fuels@0.97.1: + resolution: {integrity: sha512-k+J2w7MU5XMEOoSz2O53TI1NPIblz0uwaZ9PHPpNlH1So2+i+ZCiy83wWNYWnJr/HTB+kbvVFXY21RR3on24Cw==} engines: {node: ^18.20.3 || ^20.0.0 || ^22.0.0} hasBin: true @@ -4526,8 +4545,8 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql-request@5.0.0: - resolution: {integrity: sha512-SpVEnIo2J5k2+Zf76cUkdvIRaq5FMZvGQYnA4lUWYbc99m+fHh4CZYRRO/Ff4tCLQ613fzCm3SiDT64ubW5Gyw==} + graphql-request@6.1.0: + resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} peerDependencies: graphql: 14 - 16 @@ -4818,6 +4837,10 @@ packages: resolution: {integrity: sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==} engines: {node: '>= 0.4'} + is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -6572,6 +6595,9 @@ packages: resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==} engines: {node: '>=8'} + ts-log@2.2.7: + resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} + tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} @@ -8159,12 +8185,12 @@ snapshots: preact: 10.25.1 sha.js: 2.4.11 - '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.11.tgz(@types/react@18.3.13)(fuels@0.96.1)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.14.10.tgz(@types/react@18.3.13)(fuels@0.97.1)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@apollo/client': 3.12.3(@types/react@18.3.13)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: 1.7.9 bignumber.js: 9.1.2 - fuels: 0.96.1 + fuels: 0.97.1 graphql-ws: 5.16.0(graphql@16.9.0) tsdef: 0.0.14 optionalDependencies: @@ -8177,6 +8203,27 @@ snapshots: - react-dom - subscriptions-transport-ws + '@compolabs/spark-perpetual-ts-sdk@https://registry.npmjs.org/@compolabs/spark-perpetual-ts-sdk/-/spark-perpetual-ts-sdk-0.0.15.tgz(@types/react@18.3.13)(bufferutil@4.0.8)(fuels@0.97.1)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)': + dependencies: + '@apollo/client': 3.12.3(@types/react@18.3.13)(graphql-ws@5.16.0(graphql@16.9.0))(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@pythnetwork/pyth-evm-js': 1.82.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + axios: 1.7.9 + bignumber.js: 9.1.2 + fuels: 0.97.1 + graphql-ws: 5.16.0(graphql@16.9.0) + tsdef: 0.0.14 + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': 4.18.0 + transitivePeerDependencies: + - '@types/react' + - bufferutil + - debug + - graphql + - react + - react-dom + - subscriptions-transport-ws + - utf-8-validate + '@compolabs/tradingview-chart@1.0.21': {} '@emotion/babel-plugin@11.13.5': @@ -8570,22 +8617,22 @@ snapshots: '@ethersproject/properties': 5.7.0 '@ethersproject/strings': 5.7.0 - '@fuel-ts/abi-coder@0.96.1': + '@fuel-ts/abi-coder@0.97.1': dependencies: - '@fuel-ts/crypto': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/hasher': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/utils': 0.96.1 + '@fuel-ts/crypto': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/hasher': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/utils': 0.97.1 type-fest: 4.30.0 - '@fuel-ts/abi-typegen@0.96.1': + '@fuel-ts/abi-typegen@0.97.1': dependencies: - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/utils': 0.96.1 - '@fuel-ts/versions': 0.96.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/utils': 0.97.1 + '@fuel-ts/versions': 0.97.1 commander: 12.1.0 glob: 10.4.5 handlebars: 4.7.8 @@ -8593,142 +8640,155 @@ snapshots: ramda: 0.30.1 rimraf: 5.0.10 - '@fuel-ts/account@0.96.1': - dependencies: - '@fuel-ts/abi-coder': 0.96.1 - '@fuel-ts/address': 0.96.1 - '@fuel-ts/crypto': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/hasher': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/merkle': 0.96.1 - '@fuel-ts/transactions': 0.96.1 - '@fuel-ts/utils': 0.96.1 - '@fuel-ts/versions': 0.96.1 - '@fuels/vm-asm': 0.58.0 + '@fuel-ts/account@0.97.1': + dependencies: + '@fuel-ts/abi-coder': 0.97.1 + '@fuel-ts/address': 0.97.1 + '@fuel-ts/crypto': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/hasher': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/merkle': 0.97.1 + '@fuel-ts/transactions': 0.97.1 + '@fuel-ts/utils': 0.97.1 + '@fuel-ts/versions': 0.97.1 + '@fuels/vm-asm': 0.58.2 '@noble/curves': 1.7.0 events: 3.3.0 graphql: 16.9.0 - graphql-request: 5.0.0(graphql@16.9.0) + graphql-request: 6.1.0(graphql@16.9.0) graphql-tag: 2.12.6(graphql@16.9.0) ramda: 0.30.1 transitivePeerDependencies: - encoding - '@fuel-ts/address@0.96.1': + '@fuel-ts/address@0.97.1': dependencies: - '@fuel-ts/crypto': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/utils': 0.96.1 + '@fuel-ts/crypto': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/utils': 0.97.1 '@noble/hashes': 1.6.1 bech32: 2.0.0 - '@fuel-ts/contract@0.96.1': - dependencies: - '@fuel-ts/abi-coder': 0.96.1 - '@fuel-ts/account': 0.96.1 - '@fuel-ts/crypto': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/hasher': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/merkle': 0.96.1 - '@fuel-ts/program': 0.96.1 - '@fuel-ts/transactions': 0.96.1 - '@fuel-ts/utils': 0.96.1 - '@fuel-ts/versions': 0.96.1 - '@fuels/vm-asm': 0.58.0 + '@fuel-ts/contract@0.97.1': + dependencies: + '@fuel-ts/abi-coder': 0.97.1 + '@fuel-ts/account': 0.97.1 + '@fuel-ts/crypto': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/hasher': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/merkle': 0.97.1 + '@fuel-ts/program': 0.97.1 + '@fuel-ts/transactions': 0.97.1 + '@fuel-ts/utils': 0.97.1 + '@fuel-ts/versions': 0.97.1 + '@fuels/vm-asm': 0.58.2 ramda: 0.30.1 transitivePeerDependencies: - encoding - '@fuel-ts/crypto@0.96.1': + '@fuel-ts/crypto@0.97.1': dependencies: - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/utils': 0.96.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/utils': 0.97.1 '@noble/hashes': 1.6.1 - '@fuel-ts/errors@0.96.1': + '@fuel-ts/errors@0.97.1': dependencies: - '@fuel-ts/versions': 0.96.1 + '@fuel-ts/versions': 0.97.1 - '@fuel-ts/hasher@0.96.1': + '@fuel-ts/hasher@0.97.1': dependencies: - '@fuel-ts/crypto': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/utils': 0.96.1 + '@fuel-ts/crypto': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/utils': 0.97.1 '@noble/hashes': 1.6.1 - '@fuel-ts/interfaces@0.96.1': {} + '@fuel-ts/interfaces@0.97.1': {} - '@fuel-ts/math@0.96.1': + '@fuel-ts/math@0.97.1': dependencies: - '@fuel-ts/errors': 0.96.1 + '@fuel-ts/errors': 0.97.1 '@types/bn.js': 5.1.6 bn.js: 5.2.1 - '@fuel-ts/merkle@0.96.1': + '@fuel-ts/merkle@0.97.1': dependencies: - '@fuel-ts/hasher': 0.96.1 - '@fuel-ts/math': 0.96.1 + '@fuel-ts/hasher': 0.97.1 + '@fuel-ts/math': 0.97.1 - '@fuel-ts/program@0.96.1': + '@fuel-ts/program@0.97.1': dependencies: - '@fuel-ts/abi-coder': 0.96.1 - '@fuel-ts/account': 0.96.1 - '@fuel-ts/address': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/transactions': 0.96.1 - '@fuel-ts/utils': 0.96.1 - '@fuels/vm-asm': 0.58.0 + '@fuel-ts/abi-coder': 0.97.1 + '@fuel-ts/account': 0.97.1 + '@fuel-ts/address': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/transactions': 0.97.1 + '@fuel-ts/utils': 0.97.1 + '@fuels/vm-asm': 0.58.2 ramda: 0.30.1 transitivePeerDependencies: - encoding - '@fuel-ts/script@0.96.1': + '@fuel-ts/recipes@0.97.1': dependencies: - '@fuel-ts/abi-coder': 0.96.1 - '@fuel-ts/account': 0.96.1 - '@fuel-ts/address': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/program': 0.96.1 - '@fuel-ts/transactions': 0.96.1 - '@fuel-ts/utils': 0.96.1 + '@fuel-ts/abi-coder': 0.97.1 + '@fuel-ts/abi-typegen': 0.97.1 + '@fuel-ts/account': 0.97.1 + '@fuel-ts/contract': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/program': 0.97.1 + '@fuel-ts/transactions': 0.97.1 + '@fuel-ts/utils': 0.97.1 transitivePeerDependencies: - encoding - '@fuel-ts/transactions@0.96.1': + '@fuel-ts/script@0.97.1': dependencies: - '@fuel-ts/abi-coder': 0.96.1 - '@fuel-ts/address': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/hasher': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/utils': 0.96.1 + '@fuel-ts/abi-coder': 0.97.1 + '@fuel-ts/account': 0.97.1 + '@fuel-ts/address': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/program': 0.97.1 + '@fuel-ts/transactions': 0.97.1 + '@fuel-ts/utils': 0.97.1 + transitivePeerDependencies: + - encoding - '@fuel-ts/utils@0.96.1': + '@fuel-ts/transactions@0.97.1': dependencies: - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/versions': 0.96.1 + '@fuel-ts/abi-coder': 0.97.1 + '@fuel-ts/address': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/hasher': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/utils': 0.97.1 + + '@fuel-ts/utils@0.97.1': + dependencies: + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/versions': 0.97.1 fflate: 0.8.2 - '@fuel-ts/versions@0.96.1': + '@fuel-ts/versions@0.97.1': dependencies: chalk: 4.1.2 cli-table: 0.3.11 - '@fuels/connectors@0.36.1(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(@wagmi/connectors@5.0.26(@types/react@18.3.13)(@wagmi/core@2.12.2(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(react@18.3.1)(typescript@5.7.2)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.13)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.28.0)(typescript@5.7.2)(utf-8-validate@5.0.10)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1))(bufferutil@4.0.8)(fuels@0.96.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)': + '@fuels/connectors@0.36.1(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(@wagmi/connectors@5.0.26(@types/react@18.3.13)(@wagmi/core@2.12.2(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(react@18.3.1)(typescript@5.7.2)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.13)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.28.0)(typescript@5.7.2)(utf-8-validate@5.0.10)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1))(bufferutil@4.0.8)(fuels@0.97.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)': dependencies: '@ethereumjs/util': 9.0.3 '@ethersproject/bytes': 5.7.0 @@ -8738,7 +8798,7 @@ snapshots: '@web3modal/scaffold': 5.0.0(@types/react@18.3.13)(react@18.3.1) '@web3modal/solana': 5.0.0(@types/react@18.3.13)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10) '@web3modal/wagmi': 5.0.0(@types/react@18.3.13)(@wagmi/connectors@5.0.26(@types/react@18.3.13)(@wagmi/core@2.12.2(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(react@18.3.1)(typescript@5.7.2)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.76.3(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.13)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.28.0)(typescript@5.7.2)(utf-8-validate@5.0.10)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1))(@wagmi/core@2.13.4(@tanstack/query-core@5.62.2)(@types/react@18.3.13)(react@18.3.1)(typescript@5.7.2)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)(viem@2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1)) - fuels: 0.96.1 + fuels: 0.97.1 rpc-websockets: 7.11.0 socket.io-client: 4.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) viem: 2.20.1(bufferutil@4.0.8)(typescript@5.7.2)(utf-8-validate@5.0.10)(zod@3.24.1) @@ -8770,19 +8830,19 @@ snapshots: - vue - zod - '@fuels/react@0.36.1(@tanstack/react-query@5.62.2(react@18.3.1))(@types/react-dom@18.3.1)(@types/react@18.3.13)(fuels@0.96.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@fuels/react@0.36.1(@tanstack/react-query@5.62.2(react@18.3.1))(@types/react-dom@18.3.1)(@types/react@18.3.13)(fuels@0.97.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-dialog': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-query': 5.62.2(react@18.3.1) events: 3.3.0 - fuels: 0.96.1 + fuels: 0.97.1 react: 18.3.1 transitivePeerDependencies: - '@types/react' - '@types/react-dom' - react-dom - '@fuels/vm-asm@0.58.0': {} + '@fuels/vm-asm@0.58.2': {} '@graphql-typed-document-node/core@3.2.0(graphql@16.9.0)': dependencies: @@ -9264,6 +9324,33 @@ snapshots: transitivePeerDependencies: - axios + '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + dependencies: + '@pythnetwork/price-service-sdk': 1.7.1 + '@types/ws': 8.5.13 + axios: 1.7.9 + axios-retry: 3.9.1 + isomorphic-ws: 4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + ts-log: 2.2.7 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@pythnetwork/price-service-sdk@1.7.1': + dependencies: + bn.js: 5.2.1 + + '@pythnetwork/pyth-evm-js@1.82.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + dependencies: + '@pythnetwork/price-service-client': 1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + '@radix-ui/primitive@1.1.0': {} '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.13)(react@18.3.1)': @@ -11528,6 +11615,11 @@ snapshots: axe-core@4.10.2: {} + axios-retry@3.9.1: + dependencies: + '@babel/runtime': 7.26.0 + is-retry-allowed: 2.2.0 + axios@1.7.9: dependencies: follow-redirects: 1.15.9 @@ -12917,8 +13009,6 @@ snapshots: readable-stream: 3.6.2 webextension-polyfill: 0.10.0 - extract-files@9.0.0: {} - eyes@0.1.8: {} fancy-canvas@2.1.0: {} @@ -13040,12 +13130,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@3.0.2: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - form-data@4.0.1: dependencies: asynckit: 0.4.0 @@ -13075,24 +13159,25 @@ snapshots: fsevents@2.3.3: optional: true - fuels@0.96.1: - dependencies: - '@fuel-ts/abi-coder': 0.96.1 - '@fuel-ts/abi-typegen': 0.96.1 - '@fuel-ts/account': 0.96.1 - '@fuel-ts/address': 0.96.1 - '@fuel-ts/contract': 0.96.1 - '@fuel-ts/crypto': 0.96.1 - '@fuel-ts/errors': 0.96.1 - '@fuel-ts/hasher': 0.96.1 - '@fuel-ts/interfaces': 0.96.1 - '@fuel-ts/math': 0.96.1 - '@fuel-ts/merkle': 0.96.1 - '@fuel-ts/program': 0.96.1 - '@fuel-ts/script': 0.96.1 - '@fuel-ts/transactions': 0.96.1 - '@fuel-ts/utils': 0.96.1 - '@fuel-ts/versions': 0.96.1 + fuels@0.97.1: + dependencies: + '@fuel-ts/abi-coder': 0.97.1 + '@fuel-ts/abi-typegen': 0.97.1 + '@fuel-ts/account': 0.97.1 + '@fuel-ts/address': 0.97.1 + '@fuel-ts/contract': 0.97.1 + '@fuel-ts/crypto': 0.97.1 + '@fuel-ts/errors': 0.97.1 + '@fuel-ts/hasher': 0.97.1 + '@fuel-ts/interfaces': 0.97.1 + '@fuel-ts/math': 0.97.1 + '@fuel-ts/merkle': 0.97.1 + '@fuel-ts/program': 0.97.1 + '@fuel-ts/recipes': 0.97.1 + '@fuel-ts/script': 0.97.1 + '@fuel-ts/transactions': 0.97.1 + '@fuel-ts/utils': 0.97.1 + '@fuel-ts/versions': 0.97.1 bundle-require: 5.0.0(esbuild@0.24.0) chalk: 4.1.2 chokidar: 3.6.0 @@ -13219,12 +13304,10 @@ snapshots: graphemer@1.4.0: {} - graphql-request@5.0.0(graphql@16.9.0): + graphql-request@6.1.0(graphql@16.9.0): dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) cross-fetch: 3.1.8 - extract-files: 9.0.0 - form-data: 3.0.2 graphql: 16.9.0 transitivePeerDependencies: - encoding @@ -13501,6 +13584,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + is-retry-allowed@2.2.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.3: @@ -13572,6 +13657,10 @@ snapshots: dependencies: ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + isomorphic-ws@4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + isows@1.0.4(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -15578,6 +15667,8 @@ snapshots: dependencies: tslib: 2.8.1 + ts-log@2.2.7: {} + tsconfck@3.1.4(typescript@5.7.2): optionalDependencies: typescript: 5.7.2 diff --git a/src/App.tsx b/src/App.tsx index d960c701..cf68b4d7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,6 @@ import Header from "@components/Header"; import { HeaderPoints } from "@components/Points/HeaderPoints"; import { useClearUrlParam } from "@hooks/useClearUrlParam"; -import { usePrivateKeyAsAuth } from "@hooks/usePrivateKeyAsAuth"; import { useStores } from "@stores"; import { MODAL_TYPE } from "@stores/ModalStore"; @@ -17,6 +16,7 @@ import SideManageAssets from "@screens/Assets/SideManageAssets/SideManageAssets" import ConnectWalletDialog from "@screens/ConnectWallet"; import Dashboard from "@screens/Dashboard"; import Faucet from "@screens/Faucet"; +import PerpScreen from "@screens/PerpScreen/PerpScreen"; import SpotScreen from "@screens/SpotScreen"; import { SwapScreen } from "@screens/SwapScreen"; @@ -25,13 +25,13 @@ import { ROUTES } from "@constants"; import { FeatureToggleProvider, IntercomProvider, UnderConstructionProvider } from "@src/providers"; import { DiscordProvider } from "@src/providers/DiscordProvider"; const App: React.FC = observer(() => { - const { modalStore, tradeStore } = useStores(); + const { modalStore, marketStore } = useStores(); // This hooks is used to clear unnecessary URL parameters, // specifically "tx_id", after returning from the faucet useClearUrlParam("tx_id"); - usePrivateKeyAsAuth(); + // usePrivateKeyAsAuth(); return ( @@ -43,10 +43,11 @@ const App: React.FC = observer(() => { } path={`${ROUTES.SPOT}/:marketId`} /> + } path={`${ROUTES.PERP}/:marketId`} /> } path={ROUTES.SWAP} /> } path={ROUTES.FAUCET} /> } path="*" /> - } path={ROUTES.ROOT} /> + } path={ROUTES.ROOT} /> } path={ROUTES.DASHBOARD} /> } path={ROUTES.LEADERBOARD} /> diff --git a/src/blockchain/FuelNetwork.ts b/src/blockchain/FuelNetwork.ts index 3cd2b2a4..08190690 100644 --- a/src/blockchain/FuelNetwork.ts +++ b/src/blockchain/FuelNetwork.ts @@ -2,7 +2,8 @@ import { Account, B256Address } from "fuels"; import { makeObservable } from "mobx"; import { Nullable } from "tsdef"; -import SparkOrderBookSdk, { OrderType, WriteTransactionResponse } from "@compolabs/spark-orderbook-ts-sdk"; +import SparkOrderbookSdk, { OrderType, WriteTransactionResponse } from "@compolabs/spark-orderbook-ts-sdk"; +import SparkPerpetualSdk from "@compolabs/spark-perpetual-ts-sdk"; import BN from "@utils/BN"; import { CONFIG } from "@utils/getConfig"; @@ -16,18 +17,23 @@ export class FuelNetwork { private static instance: Nullable = null; private walletManager = new WalletManager(); - private orderbookSdk: SparkOrderBookSdk; + private orderbookSdk: SparkOrderbookSdk; + perpetualSdk: SparkPerpetualSdk; private constructor() { makeObservable(this.walletManager); - this.orderbookSdk = new SparkOrderBookSdk({ - networkUrl: CONFIG.APP.networkUrl, + this.orderbookSdk = new SparkOrderbookSdk({ + networkUrl: CONFIG.APP.links.networkUrl, contractAddresses: { - registry: CONFIG.APP.contracts.registry, - multiAsset: CONFIG.APP.contracts.multiAsset, + registry: CONFIG.SPOT.CONTRACTS.registry, + multiAsset: CONFIG.SPOT.CONTRACTS.multiAsset, }, }); + + this.perpetualSdk = new SparkPerpetualSdk({ + networkUrl: CONFIG.APP.links.networkUrl, + }); } public static getInstance(): FuelNetwork { @@ -37,14 +43,6 @@ export class FuelNetwork { return FuelNetwork.instance; } - setActiveMarket = (...params: Parameters) => { - this.orderbookSdk.setActiveMarket(...params); - }; - - setSentioConfig = (...params: Parameters) => { - this.orderbookSdk.setSentioConfig(...params); - }; - getAddress = (): Nullable => { return this.walletManager.address; }; @@ -68,10 +66,6 @@ export class FuelNetwork { return CONFIG.TOKENS; }; - getIsMainet = (): boolean => { - return CONFIG.APP.isMainnet; - }; - getTokenBySymbol = (symbol: string): Token => { return CONFIG.TOKENS_BY_SYMBOL[symbol]; }; @@ -82,97 +76,133 @@ export class FuelNetwork { connect = async (wallet: Account): Promise => { await this.walletManager.connect(wallet); + this.orderbookSdk.setActiveWallet((this.walletManager.wallet as any) ?? undefined); + this.perpetualSdk.setActiveWallet((this.walletManager.wallet as any) ?? undefined); }; connectWalletByPrivateKey = async (privateKey: string): Promise => { const provider = await this.orderbookSdk.getProvider(); await this.walletManager.connectByPrivateKey(privateKey, provider); + this.orderbookSdk.setActiveWallet((this.walletManager.wallet as any) ?? undefined); + this.perpetualSdk.setActiveWallet((this.walletManager.wallet as any) ?? undefined); }; disconnectWallet = async (): Promise => { await this.walletManager.disconnect(); + this.orderbookSdk.setActiveWallet(undefined); + this.perpetualSdk.setActiveWallet(undefined); }; addAssetToWallet = async (assetId: string): Promise => { await this.walletManager.addAsset(assetId); }; - createSpotOrder = async ( + setSpotSentioConfig = (...params: Parameters) => { + this.orderbookSdk.setSentioConfig(...params); + }; + + setSpotActiveMarket = (...params: Parameters) => { + this.orderbookSdk.setActiveMarket(...params); + }; + + setPerpActiveMarket = (...params: Parameters) => { + this.perpetualSdk.setActiveMarket(...params); + }; + + spotCreateOrder = async ( ...params: Parameters ): Promise => { return this.orderbookSdk.createOrder(...params); }; - createSpotOrderWithDeposit = async ( + spotCreateOrderWithDeposit = async ( ...params: Parameters ): Promise => { return this.orderbookSdk.createOrderWithDeposit(...params); }; - swapTokens = async ( + spotSwapTokens = async ( ...params: Parameters ): Promise => { return this.orderbookSdk.fulfillOrderMany(...params); }; - fulfillOrderManyWithDeposit = async ( + spotFulfillOrderManyWithDeposit = async ( ...params: Parameters ): Promise => { return this.orderbookSdk.fulfillOrderManyWithDeposit(...params); }; - cancelSpotOrder = async ( + spotCancelOrder = async ( ...params: Parameters ): Promise => { return this.orderbookSdk.cancelOrder(...params); }; - mintToken = async (...params: Parameters): Promise => { + spotMintToken = async ( + ...params: Parameters + ): Promise => { return this.orderbookSdk.mintToken(...params); }; - withdrawSpotBalance = async ( + spotWithdrawBalance = async ( ...params: Parameters ): Promise => { return this.orderbookSdk.withdrawAssets(...params); }; - withdrawSpotBalanceAll = async (...params: Parameters): Promise => { + spotWithdrawBalanceAll = async (...params: Parameters): Promise => { await this.orderbookSdk.withdrawAllAssets(...params); }; - depositSpotBalance = async ( + spotDepositBalance = async ( ...params: Parameters ): Promise => { return this.orderbookSdk.deposit(...params); }; - subscribeSpotOrders = (...params: Parameters) => { + spotSubscribeOrders = (...params: Parameters) => { return this.orderbookSdk.subscribeOrders(...params); }; - subscribeSpotActiveOrders = ( + perpSubscribeOrders = (...params: Parameters) => { + return this.perpetualSdk.subscribeOrders(...params); + }; + + perpSubscribeActiveOrders = ( + ...params: Parameters> + ): ReturnType> => { + return this.perpetualSdk.subscribeActiveOrders(...params); + }; + + spotSubscribeActiveOrders = ( ...params: Parameters> ): ReturnType> => { return this.orderbookSdk.subscribeActiveOrders(...params); }; - subscribeSpotTradeOrderEvents = ( + perpSubscribeTradeOrderEvents = ( + ...params: Parameters + ): ReturnType => { + return this.perpetualSdk.subscribeTradeOrderEvents(...params); + }; + + spotSubscribeTradeOrderEvents = ( ...params: Parameters ): ReturnType => { return this.orderbookSdk.subscribeTradeOrderEvents(...params); }; - fetchSpotActiveOrders = async ( + spotFetchActiveOrders = async ( ...params: Parameters ): ReturnType => { return this.orderbookSdk.fetchActiveOrders(...params); }; - fetchSpotVolume = async (...params: Parameters): Promise => { + spotFetchVolume = async (...params: Parameters): Promise => { const data = await this.orderbookSdk.fetchVolume(...params); return { @@ -182,47 +212,51 @@ export class FuelNetwork { }; }; - fetchSpotMatcherFee = async () => { + spotFetchMatcherFee = async () => { return this.orderbookSdk.fetchMatcherFee(); }; - fetchSpotProtocolFee = async () => { + spotFetchProtocolFee = async () => { return this.orderbookSdk.fetchProtocolFee(); }; - fetchSpotProtocolFeeForUser = async (...params: Parameters) => { + spotFetchProtocolFeeForUser = async (...params: Parameters) => { return this.orderbookSdk.fetchProtocolFeeForUser(...params); }; - fetchSpotProtocolFeeAmountForUser = async ( + spotFetchProtocolFeeAmountForUser = async ( ...params: Parameters ) => { return this.orderbookSdk.fetchProtocolFeeAmountForUser(...params); }; - fetchSpotUserMarketBalance = async (...params: Parameters) => { + spotFetchUserMarketBalance = async (...params: Parameters) => { return this.orderbookSdk.fetchUserMarketBalance(...params); }; - fetchUserMarketBalanceByContracts = async ( + spotFetchUserMarketBalanceByContracts = async ( ...params: Parameters ) => { return this.orderbookSdk.fetchUserMarketBalanceByContracts(...params); }; - chain = async (...params: Parameters) => { + spotChain = async (...params: Parameters) => { return this.orderbookSdk.chain(...params); }; - subscribeUserInfo = (...params: Parameters) => { + spotSubscribeUserInfo = (...params: Parameters) => { return this.orderbookSdk.subscribeUserInfo(...params); }; - fetchMinOrderSize = async () => { + subscribeActivePositions = (...params: Parameters) => { + return this.perpetualSdk.subscribeActivePositions(...params); + }; + + spotFetchMinOrderSize = async () => { return this.orderbookSdk.fetchMinOrderSize(); }; - fetchMinOrderPrice = async () => { + spotFetchMinOrderPrice = async () => { return this.orderbookSdk.fetchMinOrderPrice(); }; diff --git a/src/screens/SpotScreen/BottomTables/BaseTable.tsx b/src/components/BaseTable.tsx similarity index 95% rename from src/screens/SpotScreen/BottomTables/BaseTable.tsx rename to src/components/BaseTable.tsx index 626e5b29..75d34fda 100644 --- a/src/screens/SpotScreen/BottomTables/BaseTable.tsx +++ b/src/components/BaseTable.tsx @@ -11,8 +11,9 @@ import { media } from "@themes/breakpoints"; import { useStores } from "@stores"; import { TRADE_TABLE_SIZE } from "@stores/SettingsStore"; -import { MAX_TABLE_HEIGHT } from "./constants"; -import { TableActionButtons } from "./TableActionButtons"; +import { MAX_TABLE_HEIGHT } from "@constants"; + +import { TableActionButtons } from "../screens/SpotScreen/BottomTables/TableActionButtons"; interface Props { tabs?: { title: string; disabled: boolean; rowCount: number }[]; diff --git a/src/screens/SpotScreen/Chart/Chart.tsx b/src/components/Chart/Chart.tsx similarity index 87% rename from src/screens/SpotScreen/Chart/Chart.tsx rename to src/components/Chart/Chart.tsx index 17ecd061..eb037c6d 100644 --- a/src/screens/SpotScreen/Chart/Chart.tsx +++ b/src/components/Chart/Chart.tsx @@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useState } from "react"; import styled from "@emotion/styled"; import { observer } from "mobx-react"; +import TradingViewChartAdvance from "@components/Chart/TradingViewAdvanceWidget"; +import TradingViewWidget from "@components/Chart/TradingViewWidget"; import { Row } from "@components/Flex"; import { SmartFlex } from "@components/SmartFlex"; import Tab from "@components/Tab"; @@ -10,9 +12,6 @@ import { media } from "@themes/breakpoints"; import { useStores } from "@stores"; -import TradingViewChartAdvance from "@screens/SpotScreen/Chart/TradingViewAdvanceWidget"; -import TradingViewWidget from "@screens/SpotScreen/Chart/TradingViewWidget"; - const TABS = [ { title: "SIMPLE CHART", disabled: false }, { title: "ADVANCED CHART", disabled: false }, @@ -21,13 +20,13 @@ const TABS = [ const DISABLED_SIMPLE_CHARTS = new Set(["PSYCHO-USDC", "USDF-USDC"]); const Chart: React.FC = observer(() => { - const { tradeStore } = useStores(); + const { marketStore } = useStores(); const [activeChart, setActiveChart] = useState(0); const isSymbolDisabled = useMemo( - () => DISABLED_SIMPLE_CHARTS.has(tradeStore.market?.symbol ?? ""), - [tradeStore.market], + () => DISABLED_SIMPLE_CHARTS.has(marketStore.market?.symbol ?? ""), + [marketStore.market], ); useEffect(() => { diff --git a/src/screens/SpotScreen/Chart/TradingViewAdvanceWidget.tsx b/src/components/Chart/TradingViewAdvanceWidget.tsx similarity index 94% rename from src/screens/SpotScreen/Chart/TradingViewAdvanceWidget.tsx rename to src/components/Chart/TradingViewAdvanceWidget.tsx index 95472f49..347b4c07 100644 --- a/src/screens/SpotScreen/Chart/TradingViewAdvanceWidget.tsx +++ b/src/components/Chart/TradingViewAdvanceWidget.tsx @@ -57,13 +57,13 @@ const getLanguageFromURL = (): LanguageCode | null => { const TradingViewChartAdvance = observer(() => { const isUnderConstruction = useFlag("Trading_view_advance_stagging_"); // const isUnderConstruction = false; - + // const isUnderConstruction = useFlag("Trading_view_advance_stagging_"); const chartContainerRef = useRef() as React.MutableRefObject; - const { tradeStore, mixPanelStore, accountStore } = useStores(); + const { marketStore, mixPanelStore, accountStore } = useStores(); const bcNetwork = FuelNetwork.getInstance(); const navigate = useNavigate(); const defaultProps: Omit = { - symbol: tradeStore.market?.symbol.replace("-", ""), + symbol: marketStore.market?.symbol.replace("-", ""), interval: "D" as ResolutionString, datafeedUrl: isUnderConstruction ? "https://spark-candles.v12.trade" : "https://spark-candles.v12.trade", // После переезда 2 домен не сделали, если не появиться, можно убрать и удалить фича-флаг libraryPath: "/charting_library/", @@ -118,7 +118,7 @@ const TradingViewChartAdvance = observer(() => { token1: baseToken.symbol, token2: quoteToken.symbol, }); - tradeStore.setMarketSelectionOpened(false); + marketStore.setMarketSelectionOpened(false); navigate(`${ROUTES.SPOT}/${baseToken.symbol}-${quoteToken.symbol}`); }); } @@ -127,7 +127,7 @@ const TradingViewChartAdvance = observer(() => { return () => { tvWidget.remove(); }; - }, [tradeStore?.market]); + }, [marketStore?.market]); return
; }); diff --git a/src/screens/SpotScreen/Chart/TradingViewWidget.tsx b/src/components/Chart/TradingViewWidget.tsx similarity index 83% rename from src/screens/SpotScreen/Chart/TradingViewWidget.tsx rename to src/components/Chart/TradingViewWidget.tsx index c65fe49f..5a520778 100644 --- a/src/screens/SpotScreen/Chart/TradingViewWidget.tsx +++ b/src/components/Chart/TradingViewWidget.tsx @@ -5,9 +5,9 @@ import { observer } from "mobx-react"; import { useStores } from "@stores"; -import { SpotMarket } from "@entity"; +import { BaseMarket, PerpMarket } from "@entity"; -import ChartSkeletonWrapper from "../../../components/Skeletons/ChartSkeletonWrapper"; +import ChartSkeletonWrapper from "../Skeletons/ChartSkeletonWrapper"; const tvScriptLoadingPromise = () => new Promise((resolve) => { @@ -26,7 +26,7 @@ const CHART_CHECK_INTERVAL = 1000; // TODO: Implement logic using Market and tradingview symbol. Store symbol to get trading view chart in config -const getNormalName = (market: SpotMarket): string => { +const getNormalName = (market: BaseMarket): string => { const { baseToken, quoteToken } = market; if (baseToken.symbol === "FUEL") return "BYBIT:FUELUSDT"; @@ -45,8 +45,8 @@ const TradingViewWidget: React.FC = observer(() => { const widgetRef = useRef(); const theme = useTheme(); - const { tradeStore } = useStores(); - const { market } = tradeStore; + const { marketStore } = useStores(); + const { market } = marketStore; const createWidget = () => { const tradingViewContainer = document.getElementById(TRADING_VIEW_ID); @@ -56,7 +56,15 @@ const TradingViewWidget: React.FC = observer(() => { return; } - const symbol = getNormalName(market); + let symbol; + + if (PerpMarket.isInstance(market)) { + symbol = `OKX:BTCUSDT`; + } else { + symbol = getNormalName(market); + } + // const marketCEX = market.baseToken.symbol === "FUEL" ? "BYBIT" : "OKX"; + // const quoteTokenSymbol = market.baseToken.symbol === "FUEL" ? "USDT" : market.quoteToken.symbol; const widgetConfig = { autosize: true, diff --git a/src/screens/SpotScreen/Chart/TradingViewWidgetNew.tsx b/src/components/Chart/TradingViewWidgetNew.tsx similarity index 98% rename from src/screens/SpotScreen/Chart/TradingViewWidgetNew.tsx rename to src/components/Chart/TradingViewWidgetNew.tsx index 66ec8311..99848093 100644 --- a/src/screens/SpotScreen/Chart/TradingViewWidgetNew.tsx +++ b/src/components/Chart/TradingViewWidgetNew.tsx @@ -3,9 +3,9 @@ import styled from "@emotion/styled"; import { createChart, IChartApi } from "lightweight-charts"; import { observer } from "mobx-react-lite"; -import { useStores } from "@stores"; +import { candlestickSeriesConfig, chartConfig, histogramConfig } from "@components/Chart/configChart"; -import { candlestickSeriesConfig, chartConfig, histogramConfig } from "@screens/SpotScreen/Chart/configChart"; +import { useStores } from "@stores"; const TradingViewWidgetNew: React.FC = observer(() => { const { spotOrderBookStore } = useStores(); diff --git a/src/screens/SpotScreen/Chart/configChart.ts b/src/components/Chart/configChart.ts similarity index 100% rename from src/screens/SpotScreen/Chart/configChart.ts rename to src/components/Chart/configChart.ts diff --git a/src/screens/SpotScreen/Chart/index.ts b/src/components/Chart/index.ts similarity index 100% rename from src/screens/SpotScreen/Chart/index.ts rename to src/components/Chart/index.ts diff --git a/src/components/DepositWithdrawModal.tsx b/src/components/DepositWithdrawModal.tsx new file mode 100644 index 00000000..e9a06843 --- /dev/null +++ b/src/components/DepositWithdrawModal.tsx @@ -0,0 +1,253 @@ +import React, { useState } from "react"; +import { useTheme } from "@emotion/react"; +import styled from "@emotion/styled"; +import { observer } from "mobx-react-lite"; +import { IDialogPropTypes } from "rc-dialog/lib/IDialogPropTypes"; + +import TokenInput from "@components/TokenInput"; + +import { useStores } from "@stores"; + +import { CONFIG } from "@utils/getConfig"; + +import LeftCaretIcon from "@src/assets/icons/arrowUp.svg?react"; +import { media } from "@src/themes/breakpoints"; +import BN from "@src/utils/BN"; + +import Button, { ButtonGroup } from "./Button"; +import { Dialog } from "./Dialog"; +import MaxButton from "./MaxButton"; +import Select from "./Select"; +import { SmartFlex } from "./SmartFlex"; +import Text, { TEXT_TYPES } from "./Text"; + +export interface IProps extends IDialogPropTypes {} + +const AVAILABLE_TOKENS = CONFIG.TOKENS.filter((t) => t.collateral); +const AVAILABLE_TOKENS_SELECTOR = AVAILABLE_TOKENS.map((t) => ({ key: t.symbol, title: t.symbol, value: t })); + +const DepositWithdrawModal: React.FC = observer(({ ...rest }) => { + const { balanceStore } = useStores(); + const theme = useTheme(); + + const [isDeposit, setIsDeposit] = useState(true); + + const [depositAmount, setDepositAmount] = useState(BN.ZERO); + const [withdrawAmount, setWithdrawAmount] = useState(BN.ZERO); + + const [selectedOptionToken, setSelectedOptionToken] = useState(AVAILABLE_TOKENS_SELECTOR[0]); + + const selectedTokenBalance = balanceStore.perpContractBalances.get(selectedOptionToken.value.assetId) ?? BN.ZERO; + const selectedTokenWalletBalance = balanceStore.getWalletBalance(selectedOptionToken.value.assetId) ?? BN.ZERO; + + const selectedTokenWalletBalanceFormat = BN.formatUnits( + selectedTokenWalletBalance, + selectedOptionToken.value.decimals, + ); + const selectedTokenBalanceFormat = BN.formatUnits(selectedTokenBalance, selectedOptionToken.value.decimals); + + const isIncorrectAmount = isDeposit + ? depositAmount.gt(selectedTokenWalletBalance) + : withdrawAmount.gt(selectedTokenBalance); + + const isZero = isDeposit ? depositAmount.isZero() : withdrawAmount.isZero(); + + const shouldBeDisabled = isIncorrectAmount || isZero; + + const handleMaxClick = () => { + if (isDeposit) { + setDepositAmount(selectedTokenWalletBalance); + return; + } + + setWithdrawAmount(selectedTokenBalance); + }; + + const handleAmountChange = (v: BN) => { + if (isDeposit) { + setDepositAmount(v); + return; + } + + setWithdrawAmount(v); + }; + + const handleTokenSelect = (v: any, index: number) => { + const token = AVAILABLE_TOKENS_SELECTOR[index]; + setSelectedOptionToken(token); + }; + + const onSubmit = () => { + if (isDeposit) { + balanceStore.depositPerpBalance(selectedOptionToken.value.assetId, depositAmount); + } else { + balanceStore.withdrawPerpBalance(selectedOptionToken.value.assetId, withdrawAmount); + } + }; + + const renderTitle = () => { + return ( + + + + Deposit / Withdraw + + + ); + }; + + const renderDepositContent = () => { + return ( + + + Asset Balance + + + {selectedTokenBalanceFormat.toSignificant(2)} + + {selectedOptionToken.value.symbol} + + + + Net Account Balance (USD) + + ${BN.ZERO.toString()} + + + + ); + }; + + const renderWithdrawContent = () => { + return ( + + + + Asset balance + + + + {AVAILABLE_TOKENS.map((token) => { + const balance = balanceStore.perpContractBalances.get(token.assetId) ?? BN.ZERO; + const balanceFormat = BN.formatUnits(balance, token.decimals).toSignificant(2); + return ( + + + + + {token.symbol} + + + + {balanceFormat} + + + ); + })} + + + ); + }; + + return ( + + + + + + + + + + {renderOrderTooltip()} + + {settingsStore.orderType === ORDER_TYPE.Limit && ( + perpCreateOrderStore.setActiveInput(ACTIVE_INPUT.Price)} + /> + )} + + + perpCreateOrderStore.setActiveInput(ACTIVE_INPUT.Amount)} + /> + {settingsStore.orderType === ORDER_TYPE.Limit && ( + + + MAX + + + perpCreateOrderStore.setActiveInput(ACTIVE_INPUT.Total)} + /> + + )} + + + + Available + + + {getAvailableAmount()} + + +  {perpCreateOrderStore.isSell ? baseToken.symbol : quoteToken.symbol} + + + + {settingsStore.orderType === ORDER_TYPE.Market && ( + + Slippage + + + )} + + {/* {renderLeverageContent()} */} + {renderOrderDetails()} + + + {renderButton()} + + + + + + ); +}); + +export default CreateOrder; + +const Root = styled(SmartFlex)` + padding: 12px; + width: 100%; + min-height: 418px; + display: flex; + gap: 16px; + + ${media.mobile} { + min-height: fit-content; + } +`; + +const CreateOrderButton = styled(Button)` + margin: auto 0 0; + + ${media.mobile} { + margin: 0; + } +`; + +const ParamsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +const StyledRow = styled(SmartFlex)` + display: flex; + gap: 8px; + align-items: flex-start; +`; + +const SelectOrderTypeContainer = styled(SmartFlex)` + display: flex; + flex-direction: column; + gap: 2px; + + width: 100%; +`; + +const InputContainerWithMaxButton = styled(SelectOrderTypeContainer)` + align-items: flex-end; +`; + +const InputContainerWithError = styled(SmartFlex)` + display: flex; + gap: 8px; + align-items: flex-start; + + padding-bottom: 9px; +`; + +const StyledMaxButton = styled(MaxButton)` + position: absolute; + transform: translateY(-4px); +`; + +// const LeverageButton = styled(Button)` +// width: 34px; +// height: 32px; +// border-radius: 4px; + +// background-color: ${({ theme }) => theme.colors.bgPrimary}; +// border: 1px solid ${({ theme }) => theme.colors.borderSecondary}; +// ${TEXT_TYPES_MAP[TEXT_TYPES.BODY]} +// `; diff --git a/src/screens/PerpScreen/RightBlock/CreateOrder/OrderTypeTooltip.tsx b/src/screens/PerpScreen/RightBlock/CreateOrder/OrderTypeTooltip.tsx new file mode 100644 index 00000000..44b55f50 --- /dev/null +++ b/src/screens/PerpScreen/RightBlock/CreateOrder/OrderTypeTooltip.tsx @@ -0,0 +1,181 @@ +import React, { useState } from "react"; +import styled from "@emotion/styled"; + +import { RadioButton } from "@components/RadioButton"; +import Sheet from "@components/Sheet"; +import { SmartFlex } from "@components/SmartFlex"; +import Tab from "@components/Tab"; +import Text, { TEXT_TYPES } from "@components/Text"; +import Tooltip from "@components/Tooltip"; + +import InfoIcon from "@assets/icons/info.svg?react"; +import limitBuyChart from "@assets/tooltip/order/limitBuyChart.svg"; +import limitSellChart from "@assets/tooltip/order/limitSellChart.svg"; +import marketChart from "@assets/tooltip/order/marketChart.svg"; +import stopMarketChart from "@assets/tooltip/order/stopMarketChart.svg"; + +enum CHART_INFO { + Market, + StopMarket, + LimitBuy, + LimitSell, +} +const TABS = [ + { title: "MARKET", key: [CHART_INFO.Market] }, + { title: "LIMIT", key: [CHART_INFO.LimitBuy, CHART_INFO.LimitSell] }, +]; + +const CHART_IMG_MAP = { + [CHART_INFO.LimitBuy]: limitBuyChart, + [CHART_INFO.LimitSell]: limitSellChart, + [CHART_INFO.Market]: marketChart, + [CHART_INFO.StopMarket]: stopMarketChart, +}; + +const CHART_DESCRIPTION_MAP = { + [CHART_INFO.LimitBuy]: + "A limit order is a type of stock order where you specify the maximum price you're willing to buy or sell a stock. If the market price reaches your specified limit price, the order is executed. If not, the order remains open until your conditions are met.", + [CHART_INFO.LimitSell]: + "A limit order is a type of stock order where you specify the maximum price you're willing to buy or sell a stock. If the market price reaches your specified limit price, the order is executed. If not, the order remains open until your conditions are met.", + [CHART_INFO.Market]: + "A market order is a stock order that is executed immediately at the current market price. When you place a market order, you are prioritizing the execution speed over the specific price, so the final price at which the trade occurs may vary slightly from the current quoted price.", + [CHART_INFO.StopMarket]: + 'A stop-limit order combines features of both stop and limit orders. You set a "stop" price to trigger the order, and then a "limit" price that specifies the highest or lowest price at which you\'re willing to buy or sell. When the stop price is reached, the order becomes a limit order and will be executed only at the specified limit price or better', +}; + +const LIMIT_PAGES = new Set([CHART_INFO.LimitBuy, CHART_INFO.LimitSell]); + +const OrderType: React.FC = () => { + const [chartInfoIndex, setChartInfoIndex] = useState(CHART_INFO.Market); + + const handleSetTabIndex = (keys: CHART_INFO[]) => { + setChartInfoIndex(keys[0]); + }; + + const renderTabs = () => { + return ( + + {TABS.map((tab) => ( + handleSetTabIndex(tab.key)}> + {tab.title} + + ))} + + ); + }; + + const renderContent = () => { + const text = CHART_DESCRIPTION_MAP[chartInfoIndex]; + const img = CHART_IMG_MAP[chartInfoIndex]; + + return ( + + {renderRadioButtons()} + + {text} + + ); + }; + + const handleRadioClick = (value: string) => { + setChartInfoIndex(value === "buy" ? CHART_INFO.LimitBuy : CHART_INFO.LimitSell); + }; + + const renderRadioButtons = () => { + const isLimit = LIMIT_PAGES.has(chartInfoIndex); + + return ( + + + + + ); + }; + + return ( + + {renderTabs()} + {renderContent()} + + ); +}; + +export const OrderTypeTooltipIcon: React.FC<{ text: string; onClick?: () => void }> = ({ text, onClick }) => { + return ( + + + + {text} + + + ); +}; + +export const OrderTypeTooltip: React.FC = () => { + const [isVisible, setIsVisible] = useState(false); + + return ( + } + > + + + ); +}; + +export const OrderTypeSheet: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, onClose }) => { + return ( + + + + + + ); +}; + +const TabContainer = styled(SmartFlex)` + align-self: flex-start; + gap: 28px; + padding: 0 16px 8px; +`; + +const StyledImg = styled.img` + width: 360px; + height: 200px; +`; + +const Desc = styled(Text)` + padding: 8px 16px; + height: 120px; +`; + +const RadioContainer = styled(SmartFlex)<{ isVisible?: boolean }>` + visibility: ${({ isVisible }) => (isVisible ? "auto" : "hidden")}; + gap: 18px; + margin: 0 52px; +`; + +const InfoIconStyled = styled(InfoIcon)` + width: 12px; + height: 12px; + + & > path { + fill: ${({ theme }) => theme.colors.textDisabled}; + } +`; diff --git a/src/screens/PerpScreen/RightBlock/CreateOrder/index.ts b/src/screens/PerpScreen/RightBlock/CreateOrder/index.ts new file mode 100644 index 00000000..2b974efb --- /dev/null +++ b/src/screens/PerpScreen/RightBlock/CreateOrder/index.ts @@ -0,0 +1,3 @@ +import CreateOrder from "./CreateOrder"; + +export default CreateOrder; diff --git a/src/screens/PerpScreen/RightBlock/RightBlock.tsx b/src/screens/PerpScreen/RightBlock/RightBlock.tsx new file mode 100644 index 00000000..5f8e7923 --- /dev/null +++ b/src/screens/PerpScreen/RightBlock/RightBlock.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { observer } from "mobx-react"; + +import CreateOrder from "./CreateOrder"; + +const RightBlock: React.FC = observer(() => { + return ( + + + + ); +}); + +export default RightBlock; + +const Root = styled.div` + display: flex; + flex-direction: column; + max-width: 273px; + height: 100%; + border-radius: 10px; + background: ${({ theme }) => theme.colors.bgSecondary}; +`; diff --git a/src/screens/PerpScreen/RightBlock/index.ts b/src/screens/PerpScreen/RightBlock/index.ts new file mode 100644 index 00000000..51a3e7ac --- /dev/null +++ b/src/screens/PerpScreen/RightBlock/index.ts @@ -0,0 +1,2 @@ +import RightBlock from "./RightBlock"; +export default RightBlock; diff --git a/src/screens/PerpScreen/index.ts b/src/screens/PerpScreen/index.ts new file mode 100644 index 00000000..107b2f22 --- /dev/null +++ b/src/screens/PerpScreen/index.ts @@ -0,0 +1,2 @@ +import SpotScreen from "./PerpScreen"; +export default SpotScreen; diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx index f00e6b30..71668ee7 100644 --- a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx +++ b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx @@ -1,12 +1,106 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useTheme } from "@emotion/react"; +import { observer } from "mobx-react-lite"; -import SpotTableImpl from "./SpotTableImpl"; -import { SpotTableVMProvider } from "./SpotTableVM"; +import { Pagination } from "@components/Pagination/Pagination"; +import BottomTablesSkeletonWrapper from "@components/Skeletons/BottomTablesSkeletonWrapper"; +import Table from "@components/Table"; +import Text, { TEXT_TYPES } from "@components/Text"; -const SpotTable: React.FC = () => ( - - - -); +import { useMedia } from "@hooks/useMedia"; +import { useStores } from "@stores"; +import { PAGINATION_LIMIT } from "@stores/SpotTableStore"; + +import { BaseTable } from "../../../../components/BaseTable"; + +import { MobileTableRows, TABLE_TYPE } from "./SpotTableMobileRow"; +import { TableContainer } from "./styles"; +import { HISTORY_COLUMNS, ORDER_COLUMNS } from "./TablesHelper"; + +const PAGINATION_START_PAGE = 1; + +const SpotTable: React.FC = observer(() => { + const { accountStore, settingsStore, spotTableStore } = useStores(); + const theme = useTheme(); + const media = useMedia(); + const [tabIndex, setTabIndex] = useState(0); + const [page, setPage] = useState(PAGINATION_START_PAGE); + + const tabsConfig = [ + { + title: "ORDERS", + type: TABLE_TYPE.ORDER_DATA, + data: spotTableStore.userOrders, + count: spotTableStore.userOrdersStats?.active ?? 0, + columns: ORDER_COLUMNS(spotTableStore, theme), + }, + { + title: "HISTORY", + type: TABLE_TYPE.HISTORY, + data: spotTableStore.userOrdersHistory, + count: (spotTableStore.userOrdersStats?.closed ?? 0) + (spotTableStore.userOrdersStats?.canceled ?? 0), + columns: HISTORY_COLUMNS(theme), + }, + ]; + + const TABS = tabsConfig.map((tab) => ({ + title: tab.title, + disabled: false, + rowCount: tab.count, + })); + + useEffect(() => { + spotTableStore.resetCounter(); + }, [accountStore.isConnected]); + + const tab = tabsConfig[tabIndex]; + const data = tab.data; + const type = tab.type; + const columns = tab.columns; + + const shouldRenderPagination = data.length >= PAGINATION_LIMIT || page > PAGINATION_START_PAGE - 1; + + const handleTab = (tabIndex: number) => { + setTabIndex(tabIndex); + setPage(1); + spotTableStore.setOffset(0); + }; + + const handleChangePagination = (newPage: number) => { + setPage(newPage); + spotTableStore.setOffset(newPage); + }; + + const renderTable = () => { + if (!data.length) { + return ( + + + You haven't made any trades so far + + + Begin trading to view updates on your portfolio + + + ); + } + + return media.mobile ? : ; + }; + + return ( + + + {renderTable()} + + {shouldRenderPagination && ( + + )} + + ); +}); export default SpotTable; diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx index ac5e5bc8..e69de29b 100644 --- a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx +++ b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx @@ -1,351 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Theme, useTheme } from "@emotion/react"; -import styled from "@emotion/styled"; -import { createColumnHelper } from "@tanstack/react-table"; -import { observer } from "mobx-react"; - -import Chip from "@components/Chip"; -import { Pagination } from "@components/Pagination/Pagination"; -import { SmartFlex } from "@components/SmartFlex"; -import Table from "@components/Table"; -import Text, { TEXT_TYPES } from "@components/Text"; -import { media } from "@themes/breakpoints"; - -import { useMedia } from "@hooks/useMedia"; -import { useStores } from "@stores"; - -import { toCurrency } from "@utils/toCurrency"; - -import { SpotMarketOrder } from "@entity"; - -import BottomTablesSkeletonWrapper from "../../../../components/Skeletons/BottomTablesSkeletonWrapper"; -import { BaseTable } from "../BaseTable"; - -import { useSpotTableVMProvider } from "./SpotTableVM"; - -const orderColumnHelper = createColumnHelper(); -const tradeColumnHelper = createColumnHelper(); - -const ORDER_COLUMNS = (vm: ReturnType, theme: Theme) => [ - orderColumnHelper.accessor("timestamp", { - header: "Date", - cell: (props) => props.getValue().format("DD MMM YY, HH:mm"), - }), - orderColumnHelper.accessor("marketSymbol", { - header: "Pair", - }), - orderColumnHelper.accessor("orderType", { - header: "Type", - cell: (props) => ( - - {props.getValue()} - - ), - }), - orderColumnHelper.accessor("formatInitialAmount", { - header: "Amount", - cell: (props) => ( - - {props.getValue()} - - {props.row.original.baseToken.symbol} - - - ), - }), - orderColumnHelper.accessor("formatFilledAmount", { - id: "filled", - header: "Filled", - cell: (props) => ( - - {props.getValue()} - - {props.row.original.baseToken.symbol} - - - ), - }), - orderColumnHelper.accessor("formatPrice", { - header: "Price", - cell: (props) => toCurrency(props.getValue()), - }), - orderColumnHelper.accessor("id", { - header: "", - id: "action", - cell: (props) => ( - vm.cancelOrder(props.row.original)} - > - {vm.cancelingOrderId === props.getValue() ? "Loading..." : "Cancel"} - - ), - }), -]; - -const HISTORY_COLUMNS = (theme: Theme) => [ - tradeColumnHelper.accessor("timestamp", { - header: "Date", - cell: (props) => props.getValue().format("DD MMM YY, HH:mm"), - }), - tradeColumnHelper.accessor("marketSymbol", { - header: "Pair", - }), - tradeColumnHelper.accessor("orderType", { - header: "Type", - cell: (props) => ( - - {props.getValue()} - - ), - }), - tradeColumnHelper.accessor("formatInitialAmount", { - header: "Amount", - cell: (props) => ( - - {props.getValue()} - - {props.row.original.baseToken.symbol} - - - ), - }), - tradeColumnHelper.accessor("formatPrice", { - header: "Price", - cell: (props) => toCurrency(props.getValue()), - }), -]; - -const minNeedLengthPagination = 10; -const startPage = 1; -// todo: Упростить логику разделить формирование данных и рендер для декстопа и мобилок -const SpotTableImpl: React.FC = observer(() => { - const { accountStore, settingsStore } = useStores(); - const vm = useSpotTableVMProvider(); - const theme = useTheme(); - const media = useMedia(); - const [tabIndex, setTabIndex] = useState(0); - const columns = [ORDER_COLUMNS(vm, theme), HISTORY_COLUMNS(theme)]; - const [page, setPage] = useState(startPage); - const historyOrders = (vm.userOrdersStats?.closed ?? 0) + (vm.userOrdersStats?.canceled ?? 0); - const openOrders = vm.userOrdersStats?.active ?? 0; - const PAGINATION_LENGTH = [openOrders, historyOrders]; - const TABS = [ - { title: "ORDERS", disabled: false, rowCount: openOrders }, - { title: "HISTORY", disabled: false, rowCount: historyOrders }, - ]; - - useEffect(() => { - vm.resetCounter(); - }, [accountStore.isConnected]); - - const handleTab = (e: number) => { - setTabIndex(e); - setPage(1); - vm.setOffset(0); - }; - - const renderMobileRows = () => { - const orderData = vm.userOrders.map((ord, i) => ( - - - - {ord.marketSymbol} - - - Amount - - {ord.formatInitialAmount} - - {ord.baseToken.symbol} - - - - - - Active - - - Side: - - {ord.orderType} - - - - - - vm.cancelOrder(ord)}> - {vm.cancelingOrderId === ord.id ? "Loading..." : "Cancel"} - - - Price: - {toCurrency(ord.formatPrice)} - - - - )); - - const orderHistoryData = vm.userOrdersHistory.map((ord, i) => ( - - - - {ord.marketSymbol} - - - Amount - - {ord.formatInitialAmount} - - {ord.baseToken.symbol} - - - - - - Complete - - - Side: - - {ord.orderType} - - - - Filled: - - {ord.formatCurrentAmount} - - {ord.baseToken.symbol} - - - - - - - - Price: - {toCurrency(ord.formatPrice)} - - - - )); - - const tabToData = [orderData, orderHistoryData]; - - return ( - - {tabToData[tabIndex]} - - ); - }; - - const tabToData = [vm.userOrders, vm.userOrdersHistory]; - const data = tabToData[tabIndex]; - const handleChangePagination = (e: number) => { - vm.setOffset(e); - setPage(e); - }; - - const renderTable = () => { - if (!data.length) { - return ( - - - You haven't made any trades so far - - - Begin trading to view updates on your portfolio - - - ); - } - - if (media.mobile) { - return renderMobileRows(); - } - return
; - }; - - return ( - - - {renderTable()} - - {data.length >= minNeedLengthPagination || page > startPage - 1 ? ( - - ) : null} - {/*{!!vm.userOrders.length && tabIndex === 0 && (*/} - {/* //todo здесь была кнопка cancel all orders*/} - {/*)}*/} - - ); -}); - -export default SpotTableImpl; - -export const TableText = styled(Text)` - display: flex; - align-items: center; -`; - -const TableContainer = styled(SmartFlex)` - text-align: center; - gap: 10px; - height: 100%; - width: 100%; - padding: 32px; - ${media.mobile} { - padding: 16px; - } -`; - -const CancelButton = styled(Chip)` - cursor: pointer; - border: 1px solid ${({ theme }) => theme.colors.borderPrimary} !important; -`; - -const MobileTableOrderRow = styled(SmartFlex)` - display: grid; - grid-template-columns: repeat(3, 1fr); - width: 100%; - padding: 11px 7px 14px 7px; - background: ${({ theme }) => theme.colors.bgPrimary}; - - position: relative; - - &:not(:last-of-type)::after { - content: ""; - - position: absolute; - bottom: 0; - width: 100%; - - height: 1px; - box-shadow: inset 0 1px 0 0 ${({ theme }) => theme.colors.bgSecondary}; - } -`; - -const MobileTableRowColumn = styled(SmartFlex)` - flex-direction: column; - gap: 7px; - - &:last-of-type { - align-items: flex-end; - } -`; - -const TokenBadge = styled(SmartFlex)` - padding: 4px 8px; - background: ${({ theme }) => theme.colors.bgSecondary}; - border-radius: 4px; - - ${Text} { - line-height: 10px; - } -`; diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableMobileRow.tsx b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableMobileRow.tsx new file mode 100644 index 00000000..e3ac0861 --- /dev/null +++ b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableMobileRow.tsx @@ -0,0 +1,133 @@ +import React from "react"; +import { useTheme } from "@emotion/react"; +import { observer } from "mobx-react-lite"; + +import { SmartFlex } from "@components/SmartFlex"; +import Text, { TEXT_TYPES } from "@components/Text"; + +import { useStores } from "@stores"; + +import { toCurrency } from "@utils/toCurrency"; + +import { SpotMarketOrder } from "@entity"; + +import { CancelButton, MobileTableOrderRow, MobileTableRowColumn, TableText, TokenBadge } from "./styles"; + +export enum TABLE_TYPE { + ORDER_DATA, + HISTORY, +} + +interface Props { + type: TABLE_TYPE; + data: SpotMarketOrder[]; +} + +const OrderDataRow: React.FC = observer(({ order }) => { + const { spotTableStore } = useStores(); + const theme = useTheme(); + + return ( + + + + {order.marketSymbol} + + + Amount + + {order.formatInitialAmount} + + {order.baseToken.symbol} + + + + + + Active + + + Side: + + {order.orderType} + + + + + + spotTableStore.cancelOrder(order)}> + {order.cancelingOrderId === order.id ? "Loading..." : "Cancel"} + + + Price: + {toCurrency(order.formatPrice)} + + + + ); +}); + +const HistoryRow: React.FC = observer(({ order }) => { + const theme = useTheme(); + + return ( + + + + {order.marketSymbol} + + + Amount + + {order.formatInitialAmount} + + {order.baseToken.symbol} + + + + + + Complete + + + Side: + + {order.orderType} + + + + Filled: + + {order.formatCurrentAmount} + + {order.baseToken.symbol} + + + + + + + + Price: + {toCurrency(order.formatPrice)} + + + + ); +}); + +const ROW_TYPE_MAP = { + [TABLE_TYPE.ORDER_DATA]: OrderDataRow, + [TABLE_TYPE.HISTORY]: HistoryRow, +}; + +export const MobileTableRows: React.FC = ({ data, type }) => { + const Row = ROW_TYPE_MAP[type]; + const renderRows = data.map((order, i) => ); + + return ( + + {renderRows} + + ); +}; diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/TablesHelper.tsx b/src/screens/SpotScreen/BottomTables/SpotTable/TablesHelper.tsx new file mode 100644 index 00000000..6ca5964f --- /dev/null +++ b/src/screens/SpotScreen/BottomTables/SpotTable/TablesHelper.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { Theme } from "@emotion/react"; +import { createColumnHelper } from "@tanstack/react-table"; + +import { SmartFlex } from "@components/SmartFlex"; +import Text from "@components/Text"; + +import { SpotTableStore } from "@stores"; + +import { toCurrency } from "@utils/toCurrency"; + +import { SpotMarketOrder } from "@entity"; + +import { CancelButton, TableText, TokenBadge } from "./styles"; + +const orderColumnHelper = createColumnHelper(); +const tradeColumnHelper = createColumnHelper(); + +export const ORDER_COLUMNS = (spotTableStore: SpotTableStore, theme: Theme) => [ + orderColumnHelper.accessor("timestamp", { + header: "Date", + cell: (props) => props.getValue().format("DD MMM YY, HH:mm"), + }), + orderColumnHelper.accessor("marketSymbol", { + header: "Pair", + }), + orderColumnHelper.accessor("orderType", { + header: "Type", + cell: (props) => ( + + {props.getValue()} + + ), + }), + orderColumnHelper.accessor("formatInitialAmount", { + header: "Amount", + cell: (props) => ( + + {props.getValue()} + + {props.row.original.baseToken.symbol} + + + ), + }), + orderColumnHelper.accessor("formatFilledAmount", { + id: "filled", + header: "Filled", + cell: (props) => ( + + {props.getValue()} + + {props.row.original.baseToken.symbol} + + + ), + }), + orderColumnHelper.accessor("formatPrice", { + header: "Price", + cell: (props) => toCurrency(props.getValue()), + }), + orderColumnHelper.accessor("id", { + header: "", + id: "action", + cell: (props) => ( + spotTableStore.cancelOrder(props.row.original)} + > + {spotTableStore.cancelingOrderId === props.getValue() ? "Loading..." : "Cancel"} + + ), + }), +]; + +export const HISTORY_COLUMNS = (theme: Theme) => [ + tradeColumnHelper.accessor("timestamp", { + header: "Date", + cell: (props) => props.getValue().format("DD MMM YY, HH:mm"), + }), + tradeColumnHelper.accessor("marketSymbol", { + header: "Pair", + }), + tradeColumnHelper.accessor("orderType", { + header: "Type", + cell: (props) => ( + + {props.getValue()} + + ), + }), + tradeColumnHelper.accessor("formatInitialAmount", { + header: "Amount", + cell: (props) => ( + + {props.getValue()} + + {props.row.original.baseToken.symbol} + + + ), + }), + tradeColumnHelper.accessor("formatPrice", { + header: "Price", + cell: (props) => toCurrency(props.getValue()), + }), +]; diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/styles.ts b/src/screens/SpotScreen/BottomTables/SpotTable/styles.ts new file mode 100644 index 00000000..dbd03dfd --- /dev/null +++ b/src/screens/SpotScreen/BottomTables/SpotTable/styles.ts @@ -0,0 +1,75 @@ +import styled from "@emotion/styled"; + +import Chip from "@components/Chip"; +import { SmartFlex } from "@components/SmartFlex"; +import Text from "@components/Text"; +import { media } from "@themes/breakpoints"; + +export const TableText = styled(Text)` + display: flex; + align-items: center; +`; + +export const TableContainer = styled(SmartFlex)` + text-align: center; + gap: 10px; + height: 100%; + width: 100%; + padding: 32px; + ${media.mobile} { + padding: 16px; + } +`; + +export const CancelButton = styled(Chip)` + cursor: pointer; + border: 1px solid ${({ theme }) => theme.colors.borderPrimary} !important; +`; + +export const MobileTableOrderRow = styled(SmartFlex)` + display: grid; + grid-template-columns: repeat(3, 1fr); + width: 100%; + padding: 11px 7px 14px 7px; + background: ${({ theme }) => theme.colors.bgPrimary}; + + position: relative; + + &:not(:last-of-type)::after { + content: ""; + + position: absolute; + bottom: 0; + width: 100%; + + height: 1px; + box-shadow: inset 0 1px 0 0 ${({ theme }) => theme.colors.bgSecondary}; + } +`; + +export const MobileTableRowColumn = styled(SmartFlex)` + flex-direction: column; + gap: 7px; + + &:last-of-type { + align-items: flex-end; + } +`; + +export const TokenBadge = styled(SmartFlex)` + padding: 4px 8px; + background: ${({ theme }) => theme.colors.bgSecondary}; + border-radius: 4px; + + ${Text} { + line-height: 10px; + } +`; + +export const TextGraph = styled(Text)` + text-transform: uppercase; + + ${media.desktop} { + display: none; + } +`; diff --git a/src/screens/SpotScreen/BottomTables/TableActionButtons.tsx b/src/screens/SpotScreen/BottomTables/TableActionButtons.tsx index f7095b2b..2de2c97f 100644 --- a/src/screens/SpotScreen/BottomTables/TableActionButtons.tsx +++ b/src/screens/SpotScreen/BottomTables/TableActionButtons.tsx @@ -17,10 +17,9 @@ import TableSizeSelectorIcon from "@assets/icons/tablesSizeSelector.svg?react"; import { useStores } from "@stores"; import { TRADE_TABLE_SIZE } from "@stores/SettingsStore"; -import { Checkbox } from "@src/components/Checkbox"; +import { RESIZE_TOOLTIP_CONFIG, TABLE_SIZES_CONFIG } from "@constants"; -import { useSpotTableVMProvider } from "./SpotTable/SpotTableVM"; -import { RESIZE_TOOLTIP_CONFIG, TABLE_SIZES_CONFIG } from "./constants"; +import { Checkbox } from "@src/components/Checkbox"; const useTooltipConfig = (isVisible: boolean, setIsVisible: React.Dispatch>): Config => ({ ...RESIZE_TOOLTIP_CONFIG, @@ -29,8 +28,7 @@ const useTooltipConfig = (isVisible: boolean, setIsVisible: React.Dispatch { - const vm = useSpotTableVMProvider(); - const { settingsStore } = useStores(); + const { settingsStore, spotTableStore } = useStores(); const [isResizeTooltipVisible, setIsResizeTooltipVisible] = useState(false); const [isSettingsTooltipVisible, setIsSettingsTooltipVisible] = useState(false); @@ -62,6 +60,19 @@ export const TableActionButtons: React.FC = observer(() => { }; const renderSettingsTooltipContent = () => { + const filters = [ + { + label: "BUY", + checked: spotTableStore.filterIsBuyOrderTypeEnabled, + onChange: () => spotTableStore.toggleFilterOrderType(OrderType.Buy), + }, + { + label: "SELL", + checked: spotTableStore.filterIsSellOrderTypeEnabled, + onChange: () => spotTableStore.toggleFilterOrderType(OrderType.Sell), + }, + ]; + return ( {/* @@ -83,16 +94,13 @@ export const TableActionButtons: React.FC = observer(() => { Side - vm.toggleFilterOrderType(OrderType.Buy)}> - - BUY - - - vm.toggleFilterOrderType(OrderType.Sell)}> - - SELL - - + {filters.map((filter) => ( + + + {filter.label} + + + ))} ); diff --git a/src/screens/SpotScreen/BottomTables/constants.ts b/src/screens/SpotScreen/BottomTables/constants.ts deleted file mode 100644 index 7db45ca5..00000000 --- a/src/screens/SpotScreen/BottomTables/constants.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Config } from "react-popper-tooltip"; - -import tableLargeSize from "@assets/icons/tableLargeSize.svg"; -import tableMediumSize from "@assets/icons/tableMediumSize.svg"; -import tableSizeExtraSmall from "@assets/icons/tableSizeExtraSmall.svg"; -import tableSmallSize from "@assets/icons/tableSmallSize.svg"; - -import { TRADE_TABLE_SIZE } from "@stores/SettingsStore"; - -export const MAX_TABLE_HEIGHT = { - [TRADE_TABLE_SIZE.XS]: "120px", - [TRADE_TABLE_SIZE.S]: "197px", - [TRADE_TABLE_SIZE.M]: "263px", - [TRADE_TABLE_SIZE.L]: "395px", - [TRADE_TABLE_SIZE.AUTO]: "100%", -}; - -export const TABLE_SIZES_CONFIG = [ - { title: "Extra small", icon: tableSizeExtraSmall, size: TRADE_TABLE_SIZE.XS }, - { title: "Small", icon: tableSmallSize, size: TRADE_TABLE_SIZE.S }, - { title: "Medium", icon: tableMediumSize, size: TRADE_TABLE_SIZE.M }, - { title: "Large", icon: tableLargeSize, size: TRADE_TABLE_SIZE.L }, -]; - -export const RESIZE_TOOLTIP_CONFIG: Config = { placement: "bottom-start", trigger: "click" }; diff --git a/src/screens/SpotScreen/OrderbookAndTradesInterface/OrderbookAndTradesInterface.tsx b/src/screens/SpotScreen/OrderbookAndTrades/OrderbookAndTrades.tsx similarity index 94% rename from src/screens/SpotScreen/OrderbookAndTradesInterface/OrderbookAndTradesInterface.tsx rename to src/screens/SpotScreen/OrderbookAndTrades/OrderbookAndTrades.tsx index d67ed4be..9c723941 100644 --- a/src/screens/SpotScreen/OrderbookAndTradesInterface/OrderbookAndTradesInterface.tsx +++ b/src/screens/SpotScreen/OrderbookAndTrades/OrderbookAndTrades.tsx @@ -12,7 +12,7 @@ import OrderbookAndTradesSkeletonWrapper from "../../../components/Skeletons/Ord import { SpotOrderBook } from "./SpotOrderBook/SpotOrderBook"; import { SpotTrades } from "./SpotTrades/SpotTrades"; -const OrderbookAndTradesInterface: React.FC = observer(() => { +const OrderbookAndTrades: React.FC = observer(() => { const [isOrderbook, setIsOrderbook] = useState(true); const { spotOrderBookStore } = useStores(); @@ -39,7 +39,7 @@ const OrderbookAndTradesInterface: React.FC = observer(() => { ); }); -export default OrderbookAndTradesInterface; +export default OrderbookAndTrades; const Root = styled.div` display: flex; diff --git a/src/screens/SpotScreen/OrderbookAndTrades/SpotOrderBook/SpotOrderBook.tsx b/src/screens/SpotScreen/OrderbookAndTrades/SpotOrderBook/SpotOrderBook.tsx new file mode 100644 index 00000000..4e094b20 --- /dev/null +++ b/src/screens/SpotScreen/OrderbookAndTrades/SpotOrderBook/SpotOrderBook.tsx @@ -0,0 +1,226 @@ +import React, { HTMLAttributes } from "react"; +import styled from "@emotion/styled"; +import { observer } from "mobx-react"; +import numeral from "numeral"; + +import { Row } from "@components/Flex"; +import { SpotOrderSettingsSheet } from "@components/Modal"; +import Select from "@components/Select"; +import OrderbookAndTradesSkeletonWrapper from "@components/Skeletons/OrderbookAndTradesSkeletonWrapper"; +import { SmartFlex } from "@components/SmartFlex"; +import TableOrderBook, { ColumnProps, DataArray, SPOT_ORDER_FILTER } from "@components/TableOrderBook/TableOrderBook"; +import Text, { TEXT_TYPES } from "@components/Text"; +import { media } from "@themes/breakpoints"; + +import sellAndBuyIcon from "@assets/icons/buyAndSellOrderBookIcon.svg"; +import buyIcon from "@assets/icons/buyOrderBookIcon.svg"; +import sellIcon from "@assets/icons/sellOrderBookIcon.svg"; + +import useFlag from "@hooks/useFlag"; +import { useMedia } from "@hooks/useMedia"; +import { useStores } from "@stores"; + +import BN from "@utils/BN"; + +import { SpotMarketOrder } from "@entity"; + +interface IProps extends HTMLAttributes {} + +const SPOT_DECIMAL_OPTIONS = [0, 1, 2, 3]; + +interface OrderBookData { + firstData: DataArray[]; + secondData?: DataArray[]; +} + +const SPOT_SETTINGS_ICONS = { + [SPOT_ORDER_FILTER.SELL_AND_BUY]: sellAndBuyIcon, + [SPOT_ORDER_FILTER.SELL]: sellIcon, + [SPOT_ORDER_FILTER.BUY]: buyIcon, +}; + +export const SpotOrderBook: React.FC = observer(() => { + const { spotOrderBookStore } = useStores(); + const media = useMedia(); + const { marketStore } = useStores(); + const market = marketStore.market; + + const column: ColumnProps[] = [ + { + title: `Price ${market?.quoteToken.symbol}`, + align: "left", + }, + { + title: `Amount ${market?.baseToken.symbol}`, + align: "right", + }, + { + title: "Total", + align: "right", + }, + ]; + + const config = { + orderFilter: spotOrderBookStore.orderFilter, + isSpreadValid: spotOrderBookStore.isSpreadValid, + spreadPrice: spotOrderBookStore.spreadPrice, + spreadPercent: spotOrderBookStore.spreadPercent, + decimalGroup: spotOrderBookStore.decimalGroup, + }; + + const [isSettingsOpen, openSettings, closeSettings] = useFlag(); + + const isOrderBookEmpty = + spotOrderBookStore.allBuyOrders.length === 0 && spotOrderBookStore.allSellOrders.length === 0; + + const renderSettingsIcons = () => { + if (media.mobile) { + return ; + } + + return Object.entries(SPOT_SETTINGS_ICONS).map(([_, value], index) => ( + spotOrderBookStore.setOrderFilter(index)} + /> + )); + }; + + const indexOfDecimal = SPOT_DECIMAL_OPTIONS.indexOf(spotOrderBookStore.decimalGroup); + + const handleDecimalSelect = (index: string) => { + const value = SPOT_DECIMAL_OPTIONS[Number(index)]; + spotOrderBookStore.setDecimalGroup(value); + }; + + const renderOrders = (orders: SpotMarketOrder[], type: "sell" | "buy") => { + const newOrder = [...orders]; + newOrder.reverse(); + return newOrder.map( + (o): DataArray => [ + o.priceUnits.toFormat(spotOrderBookStore.decimalGroup), + numeral(o.currentAmountUnits).format(`0.${"0".repeat(4)}a`), + numeral(o.currentQuoteAmountUnits).format(`0.${"0".repeat(spotOrderBookStore.decimalGroup)}a`), + type === "sell", + ], + ); + }; + + if (isOrderBookEmpty) { + return ( + + No orders yet + + ); + } + + const orderBook = (): OrderBookData => { + if (spotOrderBookStore.orderFilter === SPOT_ORDER_FILTER.SELL) { + return { + firstData: renderOrders(spotOrderBookStore.sellOrders, "sell"), + }; + } + if (spotOrderBookStore.orderFilter === SPOT_ORDER_FILTER.BUY) { + return { + firstData: renderOrders(spotOrderBookStore.buyOrders, "buy"), + }; + } + return { + firstData: renderOrders(spotOrderBookStore.buyOrders, "buy"), + secondData: renderOrders(spotOrderBookStore.sellOrders, "sell"), + }; + }; + const orderBookResult: OrderBookData = orderBook(); + // spotOrderBookStore.buyOrders.map(el => console.log('formatPrice', el.formatPrice)) + return ( + + + + ({ + title: new BN(10).pow(-v).toString(), + key: index.toString(), + }))} + selected={String(indexOfDecimal)} + onSelect={({ key }) => handleDecimalSelect(key)} + /> + {renderSettingsIcons()} + + + + value)} + isOpen={isSettingsOpen} + selectedDecimal={String(indexOfDecimal)} + selectedFilter={spotOrderBookStore.orderFilter} + onClose={closeSettings} + onDecimalSelect={handleDecimalSelect} + onFilterSelect={spotOrderBookStore.setOrderFilter} + /> + + + + ); +}); + +const Root = styled(SmartFlex)` + grid-area: orderbook; + flex-direction: column; + width: 100%; + height: 100%; + gap: 8px; + + ${media.mobile} { + padding: 8px 0; + } +`; + +const SettingsContainer = styled(Row)` + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0 12px; + gap: 8px; + + ${media.mobile} { + order: 3; + padding: 0 8px; + flex-direction: row-reverse; + } +`; + +const SettingIcon = styled.img<{ selected?: boolean }>` + cursor: pointer; + transition: 0.4s; + border-radius: 4px; + border: 1px solid ${({ selected, theme }) => (selected ? theme.colors.borderAccent : "transparent")}; + + &:hover { + border: 1px solid ${({ selected, theme }) => (selected ? theme.colors.borderAccent : theme.colors.borderPrimary)}; + } +`; + +const StyledSelect = styled(Select)` + min-width: 84px; + height: 40px; +`; + +const OrderbookContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + + gap: 2px; +`; + +export { SPOT_ORDER_FILTER }; diff --git a/src/screens/SpotScreen/OrderbookAndTrades/SpotTrades/SpotTrades.tsx b/src/screens/SpotScreen/OrderbookAndTrades/SpotTrades/SpotTrades.tsx new file mode 100644 index 00000000..6b368765 --- /dev/null +++ b/src/screens/SpotScreen/OrderbookAndTrades/SpotTrades/SpotTrades.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { observer } from "mobx-react"; + +import Loader from "@components/Loader"; +import TableOrderBook, { ColumnProps, DataArray } from "@components/TableOrderBook/TableOrderBook"; + +import { useStores } from "@stores"; + +const column: ColumnProps[] = [ + { + title: "Price", + align: "left", + }, + { + title: "Qty", + align: "right", + }, + { + title: "Time", + align: "right", + }, +]; + +const config = { + orderFilter: 3, +}; + +export const SpotTrades: React.FC = observer(() => { + const { spotOrderBookStore } = useStores(); + + const isOrderBookEmpty = spotOrderBookStore.trades.length === 0; + + if (spotOrderBookStore.isTradesLoading && isOrderBookEmpty) { + return ; + } + + const formatDataTrades: DataArray[] = spotOrderBookStore.trades.map((trade) => [ + trade.formatPrice, + trade.formatTradeAmount, + trade.timestamp.format("HH:mm:ss"), + trade.sellerIsMaker, + ]); + + return ; +}); diff --git a/src/screens/SpotScreen/OrderbookAndTradesInterface/SpotOrderBook/SpotOrderBook.tsx b/src/screens/SpotScreen/OrderbookAndTradesInterface/SpotOrderBook/SpotOrderBook.tsx deleted file mode 100644 index aec08586..00000000 --- a/src/screens/SpotScreen/OrderbookAndTradesInterface/SpotOrderBook/SpotOrderBook.tsx +++ /dev/null @@ -1,408 +0,0 @@ -import React, { HTMLAttributes } from "react"; -import { useTheme } from "@emotion/react"; -import styled from "@emotion/styled"; -import { observer } from "mobx-react"; -import numeral from "numeral"; - -import { Column, Row } from "@components/Flex"; -import { SpotOrderSettingsSheet } from "@components/Modal"; -import Select from "@components/Select"; -import { SmartFlex } from "@components/SmartFlex"; -import Text, { TEXT_TYPES } from "@components/Text"; -import { media } from "@themes/breakpoints"; - -import sellAndBuyIcon from "@assets/icons/buyAndSellOrderBookIcon.svg"; -import buyIcon from "@assets/icons/buyOrderBookIcon.svg"; -import sellIcon from "@assets/icons/sellOrderBookIcon.svg"; - -import useFlag from "@hooks/useFlag"; -import { useMedia } from "@hooks/useMedia"; -import { useStores } from "@stores"; - -import BN from "@utils/BN"; -import { hexToRgba } from "@utils/hexToRgb"; - -import { SpotMarketOrder } from "@entity"; - -import OrderbookAndTradesSkeletonWrapper from "../../../../components/Skeletons/OrderbookAndTradesSkeletonWrapper"; -import { ORDER_MODE, useCreateOrderVM } from "../../RightBlock/CreateOrder/CreateOrderVM"; - -interface IProps extends HTMLAttributes {} - -export enum SPOT_ORDER_FILTER { - SELL_AND_BUY = 0, - SELL = 1, - BUY = 2, -} - -const SPOT_DECIMAL_OPTIONS = [0, 1, 2, 3]; - -const SPOT_SETTINGS_ICONS = { - [SPOT_ORDER_FILTER.SELL_AND_BUY]: sellAndBuyIcon, - [SPOT_ORDER_FILTER.SELL]: sellIcon, - [SPOT_ORDER_FILTER.BUY]: buyIcon, -}; - -export const SpotOrderBook: React.FC = observer(() => { - const { spotOrderBookStore } = useStores(); - const orderSpotVm = useCreateOrderVM(); - const media = useMedia(); - const theme = useTheme(); - const { tradeStore } = useStores(); - const market = tradeStore.market; - - const [isSettingsOpen, openSettings, closeSettings] = useFlag(); - - const isOrderBookEmpty = - spotOrderBookStore.allBuyOrders.length === 0 && spotOrderBookStore.allSellOrders.length === 0; - - const renderSettingsIcons = () => { - if (media.mobile) { - return ; - } - - return Object.entries(SPOT_SETTINGS_ICONS).map(([_, value], index) => ( - spotOrderBookStore.setOrderFilter(index)} - /> - )); - }; - - const renderSpread = () => { - let price = spotOrderBookStore.isSpreadValid ? spotOrderBookStore.spreadPrice : "-"; - price = price === "-" ? price : numeral(price).format(`0.${"0".repeat(spotOrderBookStore.decimalGroup)}a`); - const percent = spotOrderBookStore.isSpreadValid ? spotOrderBookStore.spreadPercent : "-"; - if (media.mobile) { - return ( - - - {price} - - {`(${percent}%)`} - - ); - } - - return ( - - SPREAD - {price} - {`(${percent}%) `} - - ); - }; - - const indexOfDecimal = SPOT_DECIMAL_OPTIONS.indexOf(spotOrderBookStore.decimalGroup); - - const handleDecimalSelect = (index: string) => { - const value = SPOT_DECIMAL_OPTIONS[Number(index)]; - spotOrderBookStore.setDecimalGroup(value); - }; - - const renderOrders = (orders: SpotMarketOrder[], type: "sell" | "buy") => { - const orderMode = type === "sell" ? ORDER_MODE.BUY : ORDER_MODE.SELL; - const volumePercent = (ord: SpotMarketOrder) => - type === "sell" - ? ord.initialAmount.div(spotOrderBookStore.totalSell) - : ord.initialQuoteAmount.div(spotOrderBookStore.totalBuy); - const color = type === "sell" ? theme.colors.redLight : theme.colors.greenLight; - const newOrder = [...orders]; - newOrder.reverse(); - return ( - <> - {newOrder.map((o, index) => ( - orderSpotVm.selectOrderbookOrder(o, orderMode)}> - - {o.priceUnits.toFormat(spotOrderBookStore.decimalGroup)} - {numeral(o.currentAmountUnits).format(`0.${"0".repeat(4)}a`)} - - {numeral(o.currentQuoteAmountUnits).format(`0.${"0".repeat(spotOrderBookStore.decimalGroup)}a`)} - - - ))} - - - ); - }; - - if (isOrderBookEmpty) { - return ( - - No orders yet - - ); - } - return ( - - - - ({ - title: new BN(10).pow(-v).toString(), - key: index.toString(), - }))} - selected={String(indexOfDecimal)} - onSelect={({ key }) => handleDecimalSelect(key)} - /> - {renderSettingsIcons()} - - - - {`Price ${market?.quoteToken.symbol}`} - {`Amount ${market?.baseToken.symbol}`} - Total - - - {spotOrderBookStore.orderFilter === SPOT_ORDER_FILTER.BUY && ( - {renderOrders(spotOrderBookStore.buyOrders, "buy")} - )} - {spotOrderBookStore.orderFilter === SPOT_ORDER_FILTER.SELL && ( - {renderOrders(spotOrderBookStore.sellOrders, "sell")} - )} - {spotOrderBookStore.orderFilter === SPOT_ORDER_FILTER.SELL_AND_BUY && ( - - - {renderOrders(spotOrderBookStore.sellOrders, "sell")} - - {renderSpread()} - {renderOrders(spotOrderBookStore.buyOrders, "buy")} - - )} - - - value)} - isOpen={isSettingsOpen} - selectedDecimal={String(indexOfDecimal)} - selectedFilter={spotOrderBookStore.orderFilter} - onClose={closeSettings} - onDecimalSelect={handleDecimalSelect} - onFilterSelect={spotOrderBookStore.setOrderFilter} - /> - - - - ); -}); - -const Plug: React.FC<{ - length: number; -}> = ({ length }) => ( - <> - {Array.from({ length }).map((_, index) => ( - - - - - - - - - ))} - -); - -const SmartFlexOrder = styled(SmartFlex)<{ flexDirection?: string }>` - flex-direction: ${({ flexDirection }) => flexDirection ?? "column"}; - flex-grow: 1; - width: 100%; - height: 0; - overflow: hidden; -`; - -const OrderBookColumn = styled(Column)` - height: 100%; - width: 100%; -`; - -const TextOverflow = styled(Text)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - -const PlugContainer = styled(SmartFlex)` - display: grid; - grid-template-columns: repeat(3, 1fr); - margin-bottom: 1px; - height: 16px; - width: 100%; - padding: 0 12px; - text-align: center; - gap: 10px; - - ${Text} { - text-align: start; - } - - ${media.mobile} { - grid-template-columns: repeat(2, 1fr); - - ${Text}:nth-of-type(2) { - text-align: end; - } - - ${Text}:nth-of-type(3) { - display: none; - } - } -`; - -const Root = styled(SmartFlex)` - grid-area: orderbook; - flex-direction: column; - width: 100%; - height: 100%; - gap: 8px; - - ${media.mobile} { - padding: 8px 0; - } -`; - -const SettingsContainer = styled(Row)` - justify-content: space-between; - align-items: center; - width: 100%; - padding: 0 12px; - gap: 8px; - - ${media.mobile} { - order: 3; - padding: 0 8px; - flex-direction: row-reverse; - } -`; - -const SettingIcon = styled.img<{ selected?: boolean }>` - cursor: pointer; - transition: 0.4s; - border-radius: 4px; - border: 1px solid ${({ selected, theme }) => (selected ? theme.colors.borderAccent : "transparent")}; - - &:hover { - border: 1px solid ${({ selected, theme }) => (selected ? theme.colors.borderAccent : theme.colors.borderPrimary)}; - } -`; - -const TextRightAlign = styled(Text)` - text-align: right !important; -`; - -const StyledSelect = styled(Select)` - min-width: 84px; - height: 40px; -`; - -const OrderBookHeader = styled.div` - display: grid; - grid-template-columns: repeat(3, 1fr); - width: 100%; - padding: 0 12px; - text-align: center; - height: 26px; - align-items: center; - gap: 10px; - - ${Text} { - text-align: start; - } - - ${media.mobile} { - grid-template-columns: 1fr min-content; - - ${Text}:nth-of-type(2) { - text-align: end; - } - ${TextRightAlign}:nth-of-type(2) { - display: none; - } - } -`; - -const OrderRow = styled(Row)<{ type: "buy" | "sell" }>` - position: relative; - cursor: pointer; - height: 16px; - width: 100%; - //justify-content: space-between; - //align-items: center; - padding: 0 12px; - background: transparent; - transition: 0.4s; - gap: 10px; - - &:hover { - background: ${({ type, theme }) => - type === "buy" ? hexToRgba(theme.colors.greenLight, 0.1) : hexToRgba(theme.colors.redLight, 0.1)}; - } - - ${media.mobile} { - gap: 3px; - & > ${TextRightAlign}:nth-of-type(2) { - display: none; - } - } - - & > div { - flex: 1; - text-align: left; - z-index: 1; - } -`; - -const OrderbookContainer = styled.div` - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - - gap: 2px; -`; - -const Container = styled(OrderbookContainer)<{ - fitContent?: boolean; - reverse?: boolean; -}>` - ${({ fitContent }) => !fitContent && "height: 100%;"}; - ${({ reverse }) => reverse && "flex-direction: column-reverse;"}; - ${({ reverse }) => (reverse ? "justify-content: flex-end;" : "justify-content: flex-start;")}; - height: 100%; - - ${media.mobile} { - justify-content: flex-start; - } -`; - -const SpreadContainer = styled(SmartFlex)` - padding-left: 12px; - height: 24px; - background: ${({ theme }) => theme.colors.bgPrimary}; - align-items: center; - gap: 12px; - width: 100%; -`; - -const ProgressBar = styled.span<{ type: "buy" | "sell"; fulfillPercent?: number }>` - z-index: 0; - position: absolute; - left: 0; - top: 0; - bottom: 0; - background: ${({ type, theme }) => - type === "buy" ? hexToRgba(theme.colors.greenLight, 0.1) : hexToRgba(theme.colors.redLight, 0.1)}; - transition: all 0.3s; - width: ${({ fulfillPercent }) => (fulfillPercent ? `${fulfillPercent}%` : `0%`)}; -`; - -const VolumeBar = styled(ProgressBar)<{ volumePercent?: number }>` - width: ${({ volumePercent }) => (volumePercent ? `${volumePercent}%` : `0%`)}; -`; diff --git a/src/screens/SpotScreen/OrderbookAndTradesInterface/SpotTrades/SpotTrades.tsx b/src/screens/SpotScreen/OrderbookAndTradesInterface/SpotTrades/SpotTrades.tsx deleted file mode 100644 index 97051fff..00000000 --- a/src/screens/SpotScreen/OrderbookAndTradesInterface/SpotTrades/SpotTrades.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from "react"; -import { useTheme } from "@emotion/react"; -import styled from "@emotion/styled"; -import { observer } from "mobx-react"; - -import { Column } from "@components/Flex"; -import Loader from "@components/Loader"; -import { SmartFlex } from "@components/SmartFlex"; -import Text, { TEXT_TYPES } from "@components/Text"; -import { media } from "@themes/breakpoints"; - -import { useStores } from "@stores"; - -export const SpotTrades: React.FC = observer(() => { - const { spotOrderBookStore } = useStores(); - const theme = useTheme(); - - const isOrderBookEmpty = spotOrderBookStore.trades.length === 0; - - if (spotOrderBookStore.isTradesLoading && isOrderBookEmpty) { - return ; - } - - if (isOrderBookEmpty) - return ( - - No trades yet - - ); - - return ( - -
- Price - Qty - Time -
- - - {spotOrderBookStore.trades.map((trade) => ( - - - {trade.formatPrice} - - - {trade.formatTradeAmount} - - - {trade.timestamp.format("HH:mm:ss")} - - - ))} - -
- ); -}); - -const Root = styled(Column)` - width: 100%; - height: 100%; -`; - -const Header = styled.div` - display: grid; - grid-template-columns: repeat(3, 1fr); - width: 100%; - padding: 0 12px; - text-align: center; - height: 26px; - align-items: center; - gap: 10px; - - ${Text} { - text-align: start; - } - - ${media.mobile} { - grid-template-columns: 1fr min-content; - - ${Text}:nth-of-type(2) { - text-align: end; - } - ${Text}:nth-of-type(3) { - display: none; - } - } -`; -const Container = styled.div` - display: flex; - flex-direction: column; - width: 100%; - box-sizing: border-box; - padding: 5px 12px 8px; - overflow-y: auto; - height: 100%; -`; - -const Row = styled(SmartFlex)` - display: grid; - grid-template-columns: repeat(3, 1fr); - - height: 16px; - - gap: 10px; - - align-items: center; - justify-content: space-between; -`; - -const TextRightAlign = styled(Text)` - text-align: right !important; -`; diff --git a/src/screens/SpotScreen/RightBlock/CreateOrder/CreateOrder.tsx b/src/screens/SpotScreen/RightBlock/CreateOrder/CreateOrder.tsx index 9932a52b..a7630d76 100644 --- a/src/screens/SpotScreen/RightBlock/CreateOrder/CreateOrder.tsx +++ b/src/screens/SpotScreen/RightBlock/CreateOrder/CreateOrder.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useTheme } from "@emotion/react"; import styled from "@emotion/styled"; import { Accordion } from "@szhsin/react-accordion"; import { observer } from "mobx-react-lite"; @@ -14,6 +13,7 @@ import MaxButton from "@components/MaxButton"; import { RadioButton } from "@components/RadioButton"; import Select from "@components/Select"; import SizedBox from "@components/SizedBox"; +import CreateOrderSkeletonWrapper from "@components/Skeletons/CreateOrderSkeletonWrapper"; import Slider from "@components/Slider"; import { SmartFlex } from "@components/SmartFlex"; import Text, { TEXT_TYPES } from "@components/Text"; @@ -24,13 +24,11 @@ import useFlag from "@hooks/useFlag"; import { useMedia } from "@hooks/useMedia"; import { useStores } from "@stores"; import { MIXPANEL_EVENTS } from "@stores/MixPanelStore"; +import { ACTIVE_INPUT, ORDER_MODE, ORDER_TYPE } from "@stores/SpotCreateOrderStore"; import { DEFAULT_DECIMALS, MINIMAL_ETH_REQUIRED } from "@constants"; import { getRealFee } from "@utils/getRealFee"; -import CreateOrderSkeletonWrapper from "../../../../components/Skeletons/CreateOrderSkeletonWrapper"; - -import { ACTIVE_INPUT, ORDER_MODE, ORDER_TYPE, useCreateOrderVM } from "./CreateOrderVM"; import { OrderTypeSheet, OrderTypeTooltip, OrderTypeTooltipIcon } from "./OrderTypeTooltip"; export const ORDER_OPTIONS = [ @@ -43,16 +41,23 @@ export const ORDER_OPTIONS = [ const VISIBLE_MARKET_DECIMALS = 2; const CreateOrder: React.FC = observer(() => { - const { balanceStore, tradeStore, settingsStore, spotOrderBookStore, mixPanelStore } = useStores(); - const timeInForce = settingsStore.timeInForce; - const vm = useCreateOrderVM(); - const market = tradeStore.market; + const { + balanceStore, + marketStore, + settingsStore, + spotOrderBookStore, + mixPanelStore, + spotCreateOrderStore, + spotMarketInfoStore, + } = useStores(); const media = useMedia(); - const theme = useTheme(); + + const timeInForce = settingsStore.timeInForce; + const market = marketStore.market; const dataOnboardingTradingKey = `trade-${media.mobile ? "mobile" : "desktop"}`; - const isButtonDisabled = vm.isLoading || !vm.canProceed; + const isButtonDisabled = spotCreateOrderStore.isLoading || !spotCreateOrderStore.canProceed; const isMarketOrderType = settingsStore.orderType === ORDER_TYPE.Market; const priceDisplayDecimals = isMarketOrderType ? VISIBLE_MARKET_DECIMALS : DEFAULT_DECIMALS; @@ -63,20 +68,20 @@ const CreateOrder: React.FC = observer(() => { const { baseToken, quoteToken } = market; const handlePercentChange = (v: number) => { - const token = vm.isSell ? baseToken : quoteToken; + const token = spotCreateOrderStore.isSell ? baseToken : quoteToken; - const totalBalance = balanceStore.getTotalBalance(token.assetId); + const totalBalance = balanceStore.getSpotTotalBalance(token.assetId); if (totalBalance.isZero()) return; const value = BN.percentOf(totalBalance, v); - if (vm.isSell) { - vm.setInputAmount(value); + if (spotCreateOrderStore.isSell) { + spotCreateOrderStore.setInputAmount(value); return; } - vm.setInputTotal(value); + spotCreateOrderStore.setInputTotal(value); }; const handleSetOrderType = (type: ORDER_TYPE) => { @@ -90,21 +95,26 @@ const CreateOrder: React.FC = observer(() => { const handleSetPrice = (amount: BN) => { if (settingsStore.orderType === ORDER_TYPE.Market) return; - vm.setInputPrice(amount); + spotCreateOrderStore.setInputPrice(amount); }; const handleSetSlippage = (slippage: BN) => { - vm.setInputSlippage(slippage); + spotCreateOrderStore.setInputSlippage(slippage); }; const createOrder = () => { - vm.createOrder(); + spotCreateOrderStore.createOrder(); }; const disabledOrderTypes = [ORDER_TYPE.Limit, ORDER_TYPE.LimitFOK, ORDER_TYPE.LimitIOC]; const isInputPriceDisabled = !disabledOrderTypes.includes(settingsStore.orderType); - const fee = getRealFee(tradeStore.market, tradeStore.matcherFee, tradeStore.exchangeFee, vm.mode === ORDER_MODE.SELL); + const fee = getRealFee( + marketStore.market, + spotMarketInfoStore.matcherFee, + spotMarketInfoStore.exchangeFee, + spotCreateOrderStore.isSell, + ); // TODO: Implement better solution to hide markets // const isFuelMarket = tradeStore.market?.symbol === "FUEL-USDC"; @@ -119,11 +129,11 @@ const CreateOrder: React.FC = observer(() => { // } const isEnoughGas = balanceStore.getWalletNativeBalance().gt(MINIMAL_ETH_REQUIRED); - const minimalOrder = tradeStore.minimalOrder; + const minimalOrder = spotMarketInfoStore.minimalOrder; const formatMinimalAmount = BN.formatUnits(minimalOrder.minOrder.toString(), DEFAULT_DECIMALS).toString(); const formatMinimalPrice = BN.formatUnits(minimalOrder.minPrice.toString(), DEFAULT_DECIMALS).toString(); - if (!isButtonDisabled && tradeStore.isFeeLoading) { + if (!isButtonDisabled && spotMarketInfoStore.isFeeLoading) { return ( Loading... @@ -131,7 +141,7 @@ const CreateOrder: React.FC = observer(() => { ); } - if (!isButtonDisabled && !tradeStore.getIsEnoughtMoneyForFee(vm.isSell)) { + if (!isButtonDisabled && !spotMarketInfoStore.getIsEnoughtMoneyForFee(spotCreateOrderStore.isSell)) { return ( Insufficient {quoteToken.symbol} for fee @@ -147,7 +157,7 @@ const CreateOrder: React.FC = observer(() => { ); } - if (vm.inputAmount.lt(minimalOrder.minOrder)) { + if (spotCreateOrderStore.inputAmount.lt(minimalOrder.minOrder)) { return ( Minimum amount {formatMinimalAmount} @@ -155,7 +165,7 @@ const CreateOrder: React.FC = observer(() => { ); } - if (vm.inputPrice.lt(minimalOrder.minPrice)) { + if (spotCreateOrderStore.inputPrice.lt(minimalOrder.minPrice)) { return ( Minimum price {formatMinimalPrice} @@ -167,12 +177,16 @@ const CreateOrder: React.FC = observer(() => { - {vm.isLoading ? "Loading..." : vm.isSell ? `Sell ${baseToken.symbol}` : `Buy ${baseToken.symbol}`} + {spotCreateOrderStore.isLoading + ? "Loading..." + : spotCreateOrderStore.isSell + ? `Sell ${baseToken.symbol}` + : `Buy ${baseToken.symbol}`} ); @@ -254,7 +268,9 @@ const CreateOrder: React.FC = observer(() => { Order Details - {BN.formatUnits(vm.inputAmount, baseToken.decimals).toSignificant(4)} + + {BN.formatUnits(spotCreateOrderStore.inputAmount, baseToken.decimals).toSignificant(4)} +  {baseToken.symbol}
@@ -268,9 +284,9 @@ const CreateOrder: React.FC = observer(() => { }} > - Max {vm.isSell ? "sell" : "buy"} + Max {spotCreateOrderStore.isSell ? "sell" : "buy"} - {BN.formatUnits(vm.inputTotal, quoteToken.decimals).toFormat(2)} + {BN.formatUnits(spotCreateOrderStore.inputTotal, quoteToken.decimals).toFormat(2)}  {quoteToken.symbol} @@ -291,7 +307,9 @@ const CreateOrder: React.FC = observer(() => { Total amount - {BN.formatUnits(vm.inputAmount, baseToken.decimals).toSignificant(4)} + + {BN.formatUnits(spotCreateOrderStore.inputAmount, baseToken.decimals).toSignificant(4)} +  {baseToken.symbol} @@ -301,8 +319,8 @@ const CreateOrder: React.FC = observer(() => { }; const getAvailableAmount = () => { - const token = vm.isSell ? baseToken : quoteToken; - return balanceStore.getFormatTotalBalance(token.assetId, token.decimals); + const token = spotCreateOrderStore.isSell ? baseToken : quoteToken; + return balanceStore.getSpotFormatTotalBalance(token.assetId, token.decimals); }; const onSelectOrderType = ({ key }: { key: ORDER_TYPE }) => { @@ -314,13 +332,19 @@ const CreateOrder: React.FC = observer(() => { - - @@ -338,44 +362,48 @@ const CreateOrder: React.FC = observer(() => { {settingsStore.orderType === ORDER_TYPE.Limit && ( vm.setActiveInput(ACTIVE_INPUT.Price)} + onBlur={spotCreateOrderStore.setActiveInput} + onFocus={() => spotCreateOrderStore.setActiveInput(ACTIVE_INPUT.Price)} /> )} vm.setActiveInput(ACTIVE_INPUT.Amount)} + setAmount={spotCreateOrderStore.setInputAmount} + onBlur={spotCreateOrderStore.setActiveInput} + onFocus={() => spotCreateOrderStore.setActiveInput(ACTIVE_INPUT.Amount)} /> {settingsStore.orderType === ORDER_TYPE.Limit && ( - + MAX vm.setActiveInput(ACTIVE_INPUT.Total)} + setAmount={spotCreateOrderStore.setInputTotal} + onBlur={spotCreateOrderStore.setActiveInput} + onFocus={() => spotCreateOrderStore.setActiveInput(ACTIVE_INPUT.Total)} /> )} @@ -385,9 +413,9 @@ const CreateOrder: React.FC = observer(() => { handlePercentChange(v as number)} /> @@ -397,14 +425,16 @@ const CreateOrder: React.FC = observer(() => { {getAvailableAmount()} -  {vm.isSell ? baseToken.symbol : quoteToken.symbol} + +  {spotCreateOrderStore.isSell ? baseToken.symbol : quoteToken.symbol} + {settingsStore.orderType === ORDER_TYPE.Market && ( Slippage { export default CreateOrder; -const WarningContainer = styled(SmartFlex)` - padding: 8px; - background-color: ${({ theme }) => `${theme.colors.favorite}10`}; - border-radius: 8px; -`; - const Root = styled(SmartFlex)` padding: 12px; width: 100%; diff --git a/src/screens/SpotScreen/RightBlock/MarketSelection/SpotMarketRow.tsx b/src/screens/SpotScreen/RightBlock/MarketSelection/SpotMarketRow.tsx index 0cb8cb36..e69de29b 100644 --- a/src/screens/SpotScreen/RightBlock/MarketSelection/SpotMarketRow.tsx +++ b/src/screens/SpotScreen/RightBlock/MarketSelection/SpotMarketRow.tsx @@ -1,87 +0,0 @@ -import React, { MouseEvent } from "react"; -import { useNavigate } from "react-router-dom"; -import styled from "@emotion/styled"; -import { observer } from "mobx-react"; - -import { SmartFlex } from "@components/SmartFlex"; - -import outlineStarIcon from "@assets/icons/star.svg"; -import filledStarIcon from "@assets/icons/yellowStar.svg"; - -import { useStores } from "@stores"; -import { MIXPANEL_EVENTS } from "@stores/MixPanelStore"; - -import { ROUTES } from "@constants"; - -import { SpotMarket } from "@entity"; - -import { MarketTitle } from "./MarketTitle"; - -interface IProps { - market: SpotMarket; -} - -const SpotMarketRow: React.FC = observer(({ market }) => { - const { tradeStore, mixPanelStore, accountStore } = useStores(); - const navigate = useNavigate(); - - const isFavorite = tradeStore.favMarkets.includes(market.symbol); - - const handleFavoriteClick = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const action = isFavorite ? tradeStore.removeFromFav : tradeStore.addToFav; - - action(market.symbol); - }; - - const isActive = tradeStore.market?.symbol === market.symbol; - - const handleMarketClick = () => { - mixPanelStore.trackEvent(MIXPANEL_EVENTS.CLICK_CURRENCY_PAIR, { - user_address: accountStore.address, - token1: market.baseToken.symbol, - token2: market.quoteToken.symbol, - }); - tradeStore.setMarketSelectionOpened(false); - navigate(`${ROUTES.SPOT}/${market.symbol}`); - }; - - return ( - - - - - - {/* - - $ {market.priceUnits.toFormat(market?.baseToken.precision)} - - */} - - ); -}); - -export default SpotMarketRow; - -const Root = styled.div<{ isActive: boolean }>` - display: flex; - align-items: center; - justify-content: center; - padding: 12px; - border-bottom: 1px solid ${({ theme }) => theme.colors.borderSecondary}; - box-sizing: border-box; - cursor: pointer; - background: ${({ isActive, theme }) => (isActive ? theme.colors.borderSecondary : "transparent")}; - - :hover { - background: ${({ theme }) => theme.colors.borderSecondary}; - } -`; - -const Icon = styled.img` - height: 16px; - width: 16px; - border-radius: 50%; -`; diff --git a/src/screens/SpotScreen/SpotScreen.tsx b/src/screens/SpotScreen/SpotScreen.tsx index 8fa5182f..db4d1a83 100644 --- a/src/screens/SpotScreen/SpotScreen.tsx +++ b/src/screens/SpotScreen/SpotScreen.tsx @@ -6,30 +6,18 @@ import { useMedia } from "@hooks/useMedia"; import { useStores } from "@stores"; import { MIXPANEL_EVENTS } from "@stores/MixPanelStore"; -import { CreateOrderVMProvider } from "@screens/SpotScreen/RightBlock/CreateOrder/CreateOrderVM"; - import { ROUTES } from "@constants"; import SpotScreenDesktop from "./SpotScreenDesktop"; import SpotScreenMobile from "./SpotScreenMobile"; -const SpotScreenImpl: React.FC = observer(() => { - const { tradeStore } = useStores(); - const media = useMedia(); - - useEffect(() => { - document.title = `V12 | ${tradeStore.marketSymbol}`; - }, [tradeStore.marketSymbol]); - - return media.mobile ? : ; -}); - const SpotScreen: React.FC = observer(() => { - const { tradeStore, mixPanelStore, accountStore } = useStores(); + const { marketStore, mixPanelStore, accountStore } = useStores(); const { marketId } = useParams<{ marketId: string }>(); + const media = useMedia(); useEffect(() => { - tradeStore.selectActiveMarket(marketId); + marketStore.selectActiveMarket(marketId); }, [marketId]); useEffect(() => { @@ -39,12 +27,11 @@ const SpotScreen: React.FC = observer(() => { }); }, []); - return ( - // SpotScreenImpl оборачивается в CreateOrderSpotVMProvider чтобы при нажатии на ордер в OrderbookAndTradesInterface устанавливать значение в RightBlock - - - - ); + useEffect(() => { + document.title = `V12 | ${marketStore.marketSymbol}`; + }, [marketStore.marketSymbol]); + + return media.mobile ? : ; }); export default SpotScreen; diff --git a/src/screens/SpotScreen/SpotScreenDesktop.tsx b/src/screens/SpotScreen/SpotScreenDesktop.tsx index 33f910df..c90f6f96 100644 --- a/src/screens/SpotScreen/SpotScreenDesktop.tsx +++ b/src/screens/SpotScreen/SpotScreenDesktop.tsx @@ -2,27 +2,28 @@ import React from "react"; import styled from "@emotion/styled"; import { observer } from "mobx-react"; +import Chart from "@components/Chart"; +import MarketStatisticsBar from "@components/MarketHeader"; import { SmartFlex } from "@components/SmartFlex"; +import StatusBar from "@components/StatusBar/StatusBar"; import { media } from "@themes/breakpoints"; import { useStores } from "@stores"; import BottomTables from "@screens/SpotScreen/BottomTables"; -import Chart from "@screens/SpotScreen/Chart"; -import MarketStatisticsBar from "@screens/SpotScreen/MarketStatisticsBar"; -import StatusBar from "@screens/SpotScreen/StatusBar/StatusBar"; -import OrderbookAndTradesInterface from "./OrderbookAndTradesInterface/OrderbookAndTradesInterface"; -import MarketSelection from "./RightBlock/MarketSelection"; +import MarketSelection from "../../components/MarketSelection"; + +import OrderbookAndTradesInterface from "./OrderbookAndTrades/OrderbookAndTrades"; import RightBlock from "./RightBlock/RightBlock"; const SpotScreenDesktop: React.FC = observer(() => { - const { tradeStore } = useStores(); + const { marketStore } = useStores(); return ( - {tradeStore.marketSelectionOpened && } + {marketStore.marketSelectionOpened && } diff --git a/src/screens/SpotScreen/SpotScreenMobile.tsx b/src/screens/SpotScreen/SpotScreenMobile.tsx index 077b2b78..03e83d8f 100644 --- a/src/screens/SpotScreen/SpotScreenMobile.tsx +++ b/src/screens/SpotScreen/SpotScreenMobile.tsx @@ -2,24 +2,25 @@ import React, { useState } from "react"; import styled from "@emotion/styled"; import { observer } from "mobx-react"; +import Chart from "@components/Chart"; +import MarketStatisticsBar from "@components/MarketHeader"; import MenuOverlay from "@components/MenuOverlay"; import { SmartFlex } from "@components/SmartFlex"; +import StatusBar from "@components/StatusBar/StatusBar"; import { media } from "@themes/breakpoints"; import { useStores } from "@stores"; import BottomTables from "@screens/SpotScreen/BottomTables"; -import Chart from "@screens/SpotScreen/Chart"; -import MarketStatisticsBar from "@screens/SpotScreen/MarketStatisticsBar"; -import StatusBar from "@screens/SpotScreen/StatusBar/StatusBar"; -import { SpotOrderBook } from "./OrderbookAndTradesInterface/SpotOrderBook/SpotOrderBook"; +import MarketSelection from "../../components/MarketSelection"; +import MarketStatistics from "../../components/MarketStatistics"; + +import { SpotOrderBook } from "./OrderbookAndTrades/SpotOrderBook/SpotOrderBook"; import CreateOrder from "./RightBlock/CreateOrder"; -import MarketSelection from "./RightBlock/MarketSelection"; -import MarketStatistics from "./MarketStatistics"; const SpotScreenMobile: React.FC = observer(() => { - const { tradeStore } = useStores(); + const { marketStore } = useStores(); const [isChartOpen, setIsChartOpen] = useState(false); const handleToggleChart = () => { @@ -59,7 +60,7 @@ const SpotScreenMobile: React.FC = observer(() => { {renderContent()} - + diff --git a/src/screens/SwapScreen/InfoBlock.tsx b/src/screens/SwapScreen/InfoBlock.tsx index d9250f32..4d71f9f0 100644 --- a/src/screens/SwapScreen/InfoBlock.tsx +++ b/src/screens/SwapScreen/InfoBlock.tsx @@ -22,21 +22,24 @@ interface InfoBlockProps { } export const InfoBlock: React.FC = ({ slippage, updateSlippage }) => { + const { swapStore, oracleStore } = useStores(); const theme = useTheme(); + const [showDetails, setShowDetails] = useState(false); const [isSlippageSettingOpen, setSlippageSettingOpen] = useState(false); - const { swapStore, oracleStore } = useStores(); + const exchangeRate = oracleStore .getTokenIndexPrice(swapStore.sellToken.priceFeed) .dividedBy(oracleStore.getTokenIndexPrice(swapStore.buyToken.priceFeed)) .toNumber(); // TODO: Fix it - // const exchangeFee = tradeStore.exchangeFeeFormat; - // const matcherFee = tradeStore.matcherFeeFormat; + // const exchangeFee = spotMarketInfoStore.exchangeFeeFormat; + // const matcherFee = spotMarketInfoStore.matcherFeeFormat; const exchangeFee = BN.ZERO; const matcherFee = BN.ZERO; const totalFee = exchangeFee.plus(matcherFee); + return ( setShowDetails(!showDetails)}> diff --git a/src/screens/SwapScreen/SwapScreen.tsx b/src/screens/SwapScreen/SwapScreen.tsx index 7c84962f..c13bca81 100644 --- a/src/screens/SwapScreen/SwapScreen.tsx +++ b/src/screens/SwapScreen/SwapScreen.tsx @@ -22,7 +22,7 @@ import { DEFAULT_DECIMALS, MINIMAL_ETH_REQUIRED, ROUTES } from "@constants"; import BN from "@utils/BN"; import { isValidAmountInput, parseNumberWithCommas, replaceComma } from "@utils/swapUtils"; -import { Token } from "@entity"; +import { SpotMarket, Token } from "@entity"; import SwapButtonSkeletonWrapper from "../../components/Skeletons/SwapButtonSkeletonWrapper"; import SwapSkeletonWrapper from "../../components/Skeletons/SwapSkeletonWrapper"; @@ -34,7 +34,7 @@ import { TokenSelect } from "./TokenSelect"; const INITIAL_SLIPPAGE = 1; export const SwapScreen: React.FC = observer(() => { - const { swapStore, balanceStore, tradeStore, spotOrderBookStore, mixPanelStore, accountStore } = useStores(); + const { swapStore, balanceStore, marketStore, spotOrderBookStore, mixPanelStore, accountStore } = useStores(); const { isConnected } = useWallet(); const theme = useTheme(); const media = useMedia(); @@ -50,7 +50,7 @@ export const SwapScreen: React.FC = observer(() => { const [isLoading, setIsloading] = useState(false); const [onPress, setOnPress] = useState(false); const tokens = swapStore.fetchNewTokens(); - const markets = tradeStore.spotMarkets; + const markets = marketStore.markets.filter((m) => SpotMarket.isInstance(m)); const buyTokenOptions = useMemo(() => { return markets .filter( @@ -168,7 +168,7 @@ export const SwapScreen: React.FC = observer(() => { swapStore.setReceiveAmount("0"); const marketId = swapStore.getMarketPair(pair, paris[0]); if (!marketId) return; - tradeStore.selectActiveMarket(marketId.symbol); + marketStore.selectActiveMarket(marketId.symbol); balanceStore.update(); }; diff --git a/src/stores/AccountStore.ts b/src/stores/AccountStore.ts index 1e873564..dc2a1b96 100644 --- a/src/stores/AccountStore.ts +++ b/src/stores/AccountStore.ts @@ -10,7 +10,7 @@ export interface ISerializedAccountStore { privateKey: Nullable; } -class AccountStore { +export class AccountStore { initialized = false; constructor( @@ -88,5 +88,3 @@ class AccountStore { }; }; } - -export default AccountStore; diff --git a/src/stores/BalanceStore.ts b/src/stores/BalanceStore.ts index abc24d74..786a4794 100644 --- a/src/stores/BalanceStore.ts +++ b/src/stores/BalanceStore.ts @@ -2,6 +2,7 @@ import { Address } from "fuels"; import { makeAutoObservable, reaction, runInAction } from "mobx"; import { UserMarketBalance } from "@compolabs/spark-orderbook-ts-sdk"; +import { Deposit } from "@compolabs/spark-perpetual-ts-sdk"; import { DEFAULT_DECIMALS } from "@constants"; import BN from "@utils/BN"; @@ -16,11 +17,12 @@ import { Balances } from "@blockchain/types"; import RootStore from "./RootStore"; -const UPDATE_INTERVAL = 5 * 1000; +const UPDATE_INTERVAL = 15 * 1000; export class BalanceStore { public balances: Map = new Map(); - public contractBalances: Map = new Map(); + public spotContractBalances: Map = new Map(); + public perpContractBalances: Map = new Map(); public initialized = false; private balancesUpdater: IntervalUpdater; @@ -38,7 +40,8 @@ export class BalanceStore { ([isConnected]) => { if (!isConnected) { this.balances = new Map(); - this.contractBalances = new Map(); + this.spotContractBalances = new Map(); + this.perpContractBalances = new Map(); this.initialized = false; return; } @@ -50,6 +53,7 @@ export class BalanceStore { } initialize = async () => { + console.log("init"); await this.update(); runInAction(() => { this.initialized = true; @@ -64,14 +68,14 @@ export class BalanceStore { return tokens.map((token) => { const balance = this.getWalletBalance(token.assetId); - const contractBalance = this.getContractBalance(token.assetId); - const totalBalance = balance.plus(contractBalance); + const spotContractBalance = this.getSpotContractBalance(token.assetId); + const totalBalance = balance.plus(spotContractBalance); return { assetId: token.assetId, asset: token, walletBalance: BN.formatUnits(balance, token.decimals).toString(), - contractBalance: BN.formatUnits(contractBalance, token.decimals).toString(), + contractBalance: BN.formatUnits(spotContractBalance, token.decimals).toString(), balance: BN.formatUnits(totalBalance, token.decimals).toString(), price: BN.formatUnits(oracleStore.getTokenIndexPrice(token.priceFeed), DEFAULT_DECIMALS).toString(), }; @@ -80,7 +84,7 @@ export class BalanceStore { clearBalance = () => { this.balances = new Map(); - this.contractBalances = new Map(); + this.spotContractBalances = new Map(); }; update = async () => { @@ -91,10 +95,10 @@ export class BalanceStore { if (!accountStore.address || !wallet) return; const address = Address.fromB256(accountStore.address); - - const [balances, contractBalances] = await Promise.all([ + const [balances, spotContractBalances, perpContractBalances] = await Promise.all([ this.fetchUserBalances(), - this.fetchUserContractBalances(address), + this.fetchUserSpotContractBalances(address), + this.fetchUserPerpVaultBalances(), ]); try { @@ -108,31 +112,25 @@ export class BalanceStore { }); } - const aggregatedBalances = CONFIG.MARKETS.reduce( - (acc, market, index) => { - const marketBalance = contractBalances[index]; + const aggregatedSpotBalances = CONFIG.SPOT.MARKETS.reduce>((acc, market, index) => { + const marketBalance = spotContractBalances[index]; - const baseAmount = marketBalance ? new BN(marketBalance.liquid.base) : BN.ZERO; - const quoteAmount = marketBalance ? new BN(marketBalance.liquid.quote) : BN.ZERO; + const baseAmount = marketBalance ? new BN(marketBalance.liquid.base) : BN.ZERO; + const quoteAmount = marketBalance ? new BN(marketBalance.liquid.quote) : BN.ZERO; - if (!acc[market.baseAssetId]) { - acc[market.baseAssetId] = BN.ZERO; - } - acc[market.baseAssetId] = acc[market.baseAssetId].plus(baseAmount); + acc[market.baseAssetId] = (acc[market.baseAssetId] || BN.ZERO).plus(baseAmount); + acc[market.quoteAssetId] = (acc[market.quoteAssetId] || BN.ZERO).plus(quoteAmount); - if (!acc[market.quoteAssetId]) { - acc[market.quoteAssetId] = BN.ZERO; - } - acc[market.quoteAssetId] = acc[market.quoteAssetId].plus(quoteAmount); + return acc; + }, {}); - return acc; - }, - {} as Record, - ); + const aggregatedPerpBalances = perpContractBalances.reduce>((acc, vault) => { + acc[vault.token.assetId] = (acc[vault.token.assetId] ?? BN.ZERO).plus(vault.balance); + return acc; + }, {}); - Object.entries(aggregatedBalances).forEach(([assetId, balance]) => { - this.contractBalances.set(assetId, balance); - }); + this.spotContractBalances = new Map(Object.entries(aggregatedSpotBalances)); + this.perpContractBalances = new Map(Object.entries(aggregatedPerpBalances)); } catch (error) { console.error("Error updating user balances:", error); } @@ -150,27 +148,39 @@ export class BalanceStore { return this.balances.get(CONFIG.TOKENS_BY_SYMBOL.ETH.assetId) ?? BN.ZERO; }; - getContractBalance = (assetId: string) => { - const amount = this.contractBalances.get(assetId); - return amount ?? BN.ZERO; + getSpotTotalBalance = (assetId: string) => { + const walletBalance = this.balances.get(assetId) ?? BN.ZERO; + const spotContractBalance = this.spotContractBalances.get(assetId) ?? BN.ZERO; + + return walletBalance.plus(spotContractBalance); }; - getFormatContractBalance = (assetId: string, decimals: number) => { - return BN.formatUnits(this.getContractBalance(assetId), decimals).toSignificant(2) ?? "-"; + getSpotFormatTotalBalance = (assetId: string, decimals: number) => { + return BN.formatUnits(this.getSpotTotalBalance(assetId), decimals).toSignificant(2) ?? "-"; }; - getTotalBalance = (assetId: string) => { - const walletBalance = this.balances.get(assetId) ?? BN.ZERO; - const contractBalance = this.contractBalances.get(assetId) ?? BN.ZERO; + getPerpTotalBalance = (assetId: string) => { + // const walletBalance = this.balances.get(assetId) ?? BN.ZERO; + const perpContractBalance = this.perpContractBalances.get(assetId) ?? BN.ZERO; + + // return walletBalance.plus(perpContractBalances); + return perpContractBalance; + }; + + getPerpFormatTotalBalance = (assetId: string, decimals: number) => { + return BN.formatUnits(this.getPerpTotalBalance(assetId), decimals).toSignificant(2) ?? "-"; + }; - return walletBalance.plus(contractBalance); + getSpotContractBalance = (assetId: string) => { + const amount = this.spotContractBalances.get(assetId); + return amount ?? BN.ZERO; }; - getFormatTotalBalance = (assetId: string, decimals: number) => { - return BN.formatUnits(this.getTotalBalance(assetId), decimals).toSignificant(2) ?? "-"; + getFormatSpotContractBalance = (assetId: string, decimals: number) => { + return BN.formatUnits(this.getSpotContractBalance(assetId), decimals).toSignificant(2) ?? "-"; }; - depositBalance = async (assetId: string, amount: string) => { + depositSpotBalance = async (assetId: string, amount: string) => { const { notificationStore } = this.rootStore; const bcNetwork = FuelNetwork.getInstance(); @@ -182,7 +192,45 @@ export class BalanceStore { const token = bcNetwork.getTokenByAssetId(assetId); const amountFormatted = BN.formatUnits(amount, token.decimals).toSignificant(2); try { - const tx = await bcNetwork?.depositSpotBalance(token, amount); + const tx = await bcNetwork?.spotDepositBalance(token, amount); + notificationStore.success({ + text: getActionMessage(ACTION_MESSAGE_TYPE.DEPOSITING_TOKENS)(amountFormatted, token.symbol), + hash: tx.transactionId, + }); + return true; + } catch (error: any) { + handleWalletErrors( + notificationStore, + error, + getActionMessage(ACTION_MESSAGE_TYPE.DEPOSITING_TOKENS_FAILED)(amountFormatted, token.symbol), + ); + return false; + } + }; + + depositPerpBalance = async (assetId: string, amount: BN) => { + const { notificationStore } = this.rootStore; + const bcNetwork = FuelNetwork.getInstance(); + + if (bcNetwork?.getIsExternalWallet()) { + notificationStore.info({ + text: "Please, confirm operation in your wallet", + }); + } + const token = bcNetwork.getTokenByAssetId(assetId); + + const amountBN = BN.formatUnits(amount, token.decimals); + const amountFormatted = amountBN.toSignificant(2); + + const vaultContract = await bcNetwork.perpetualSdk.getClearingHouseContract(CONFIG.PERP.CONTRACTS.clearingHouse); + + const deposit: Deposit = { + amount, + token: token.assetId, + }; + + try { + const tx = await vaultContract.depositCollateralC(deposit); notificationStore.success({ text: getActionMessage(ACTION_MESSAGE_TYPE.DEPOSITING_TOKENS)(amountFormatted, token.symbol), hash: tx.transactionId, @@ -198,9 +246,9 @@ export class BalanceStore { } }; - withdrawBalance = async (assetId: string, amount: string) => { + withdrawSpotBalance = async (assetId: string, amount: string) => { const { notificationStore } = this.rootStore; - const markets = CONFIG.MARKETS.filter((el) => el.baseAssetId === assetId || el.quoteAssetId === assetId); + const markets = CONFIG.SPOT.MARKETS.filter((el) => el.baseAssetId === assetId || el.quoteAssetId === assetId); const bcNetwork = FuelNetwork.getInstance(); @@ -223,7 +271,7 @@ export class BalanceStore { } try { - const tx = await bcNetwork?.withdrawSpotBalance( + const tx = await bcNetwork?.spotWithdrawBalance( type, markets.map((m) => m.contractId), amount, @@ -243,7 +291,7 @@ export class BalanceStore { } }; - withdrawBalanceAll = async () => { + withdrawSpotBalanceAll = async () => { const { notificationStore } = this.rootStore; const bcNetwork = FuelNetwork.getInstance(); @@ -251,10 +299,10 @@ export class BalanceStore { notificationStore.info({ text: "Please, confirm operation in your wallet" }); } - const markets = CONFIG.MARKETS.map((el) => el.contractId); + const markets = CONFIG.SPOT.MARKETS.map((el) => el.contractId); try { - await bcNetwork?.withdrawSpotBalanceAll(markets); + await bcNetwork?.spotWithdrawBalanceAll(markets); notificationStore.success({ text: getActionMessage(ACTION_MESSAGE_TYPE.WITHDRAWING_ALL_TOKENS)(), hash: "", @@ -270,18 +318,72 @@ export class BalanceStore { } }; + withdrawPerpBalance = async (assetId: string, amount: BN) => { + const { notificationStore } = this.rootStore; + const bcNetwork = FuelNetwork.getInstance(); + + if (bcNetwork?.getIsExternalWallet()) { + notificationStore.info({ + text: "Please, confirm operation in your wallet", + }); + } + const token = bcNetwork.getTokenByAssetId(assetId); + + const amountBN = BN.formatUnits(amount, token.decimals); + const amountFormatted = amountBN.toSignificant(2); + + const vaultContract = await bcNetwork.perpetualSdk.getClearingHouseContract(CONFIG.PERP.CONTRACTS.clearingHouse); + + try { + const tx = await vaultContract.withdrawCollateralC(assetId, amount, bcNetwork.getAddress()!); + notificationStore.success({ + text: getActionMessage(ACTION_MESSAGE_TYPE.WITHDRAWING_TOKENS)(amountFormatted, token.symbol), + hash: tx.transactionId, + }); + return true; + } catch (error: any) { + handleWalletErrors( + notificationStore, + error, + getActionMessage(ACTION_MESSAGE_TYPE.WITHDRAWING_TOKENS_FAILED)(amountFormatted, token.symbol), + ); + return false; + } + }; + private fetchUserBalances = async (): Promise => { const bcNetwork = FuelNetwork.getInstance(); return bcNetwork.getBalances(); }; - private fetchUserContractBalances = async (address: Address): Promise => { + private fetchUserSpotContractBalances = async (address: Address): Promise => { const bcNetwork = FuelNetwork.getInstance(); - return bcNetwork.fetchUserMarketBalanceByContracts( + return bcNetwork.spotFetchUserMarketBalanceByContracts( address.bech32Address, - CONFIG.MARKETS.map((m) => m.contractId), + CONFIG.SPOT.MARKETS.map((m) => m.contractId), ); }; + + private fetchUserPerpVaultBalances = async () => { + const bcNetwork = FuelNetwork.getInstance(); + + const vaultContract = await bcNetwork.perpetualSdk.getVaultContract(CONFIG.PERP.CONTRACTS.vault, true); + + const balanceRequests = CONFIG.TOKENS.map(async (token) => { + const balance = await vaultContract + .getCollateralBalance(bcNetwork.getAddress()!, token.assetId) + .catch(() => BN.ZERO); + + return { + token, + balance, + }; + }); + + const balances = await Promise.all(balanceRequests); + + return balances; + }; } diff --git a/src/stores/DashboardStore.ts b/src/stores/DashboardStore.ts index d1645701..538dffc8 100644 --- a/src/stores/DashboardStore.ts +++ b/src/stores/DashboardStore.ts @@ -28,7 +28,7 @@ export interface DataPoint { value: number; } -class DashboardStore { +export class DashboardStore { initialized = false; rowSnapshots: RowSnapshot[] = []; tradeEvents: RowTradeEvent[] = []; @@ -141,10 +141,10 @@ class DashboardStore { toTimestamp: this.calculateTime(new Date(), 0), }; const config = { - url: CONFIG.APP.sentioUrl, + url: CONFIG.APP.links.sentioUrl, apiKey: "TLjw41s3DYbWALbwmvwLDM9vbVEDrD9BP", }; - bcNetwork.setSentioConfig(config); + bcNetwork.setSpotSentioConfig(config); const data = await bcNetwork.getUserScoreSnapshot(params); this.rowSnapshots = data?.result?.rows ?? []; }; @@ -159,13 +159,11 @@ class DashboardStore { toTimestamp: this.calculateTime(new Date(), 0), }; const config = { - url: CONFIG.APP.sentioUrl, + url: CONFIG.APP.links.sentioUrl, apiKey: "TLjw41s3DYbWALbwmvwLDM9vbVEDrD9BP", }; - bcNetwork.setSentioConfig(config); + bcNetwork.setSpotSentioConfig(config); const data = await bcNetwork.getTradeEvent(params); this.tradeEvents = data?.result?.rows ?? []; }; } - -export default DashboardStore; diff --git a/src/stores/FaucetStore.ts b/src/stores/FaucetStore.ts index f689dad4..fa5446c7 100644 --- a/src/stores/FaucetStore.ts +++ b/src/stores/FaucetStore.ts @@ -1,8 +1,6 @@ import { makeAutoObservable } from "mobx"; import { Nullable } from "tsdef"; -import RootStore from "@stores/RootStore"; - import { FUEL_FAUCET } from "@constants"; import BN from "@utils/BN"; import { ACTION_MESSAGE_TYPE, getActionMessage } from "@utils/getActionMessage"; @@ -10,6 +8,8 @@ import { handleWalletErrors } from "@utils/handleWalletErrors"; import { FuelNetwork } from "@blockchain"; +import RootStore from "./RootStore"; + export const FAUCET_AMOUNTS: Record = { ETH: 0.002, USDC: 3000, @@ -18,7 +18,7 @@ export const FAUCET_AMOUNTS: Record = { }; const AVAILABLE_TOKENS = ["ETH", "UNI", "USDC"]; -class FaucetStore { +export class FaucetStore { private readonly rootStore: RootStore; loading: boolean = false; @@ -75,7 +75,7 @@ class FaucetStore { try { const amount = FAUCET_AMOUNTS[token.symbol].toString(); - const tx = await bcNetwork?.mintToken(token, amount); + const tx = await bcNetwork?.spotMintToken(token, amount); notificationStore.success({ text: getActionMessage(ACTION_MESSAGE_TYPE.MINTING_TEST_TOKENS)(amount, token.symbol), hash: tx.transactionId, @@ -120,5 +120,3 @@ class FaucetStore { private setLoading = (l: boolean) => (this.loading = l); } - -export default FaucetStore; diff --git a/src/stores/LeaderboardStore.ts b/src/stores/LeaderboardStore.ts index 52e6546e..13435184 100644 --- a/src/stores/LeaderboardStore.ts +++ b/src/stores/LeaderboardStore.ts @@ -3,18 +3,18 @@ import { makeAutoObservable, reaction } from "mobx"; import { GetLeaderboardQueryParams, TraderVolumeResponse } from "@compolabs/spark-orderbook-ts-sdk"; -import { FiltersProps } from "@stores/DashboardStore.ts"; +import { FiltersProps } from "@stores/DashboardStore"; import { filters } from "@screens/Dashboard/const"; -import { CONFIG } from "@utils/getConfig.ts"; +import { CONFIG } from "@utils/getConfig"; import { FuelNetwork } from "@blockchain"; import RootStore from "./RootStore"; const config = { - url: CONFIG.APP.sentioUrl, + url: CONFIG.APP.links.sentioUrl, apiKey: "TLjw41s3DYbWALbwmvwLDM9vbVEDrD9BP", }; @@ -30,7 +30,7 @@ export const PAGINATION_PER_PAGE: PaginationProps[] = [ { title: "100", key: 100 }, ]; -class LeaderboardStore { +export class LeaderboardStore { initialized = false; activeUserStat = 0; activeTime = 0; @@ -70,7 +70,7 @@ class LeaderboardStore { currentTimestamp: Math.floor(new Date().getTime() / 1000), interval: this.activeFilter.value * 3600, }; - bcNetwork.setSentioConfig(config); + bcNetwork.setSpotSentioConfig(config); const data = await bcNetwork.getLeaderboard(params); const mainData = data?.result?.rows ?? []; @@ -154,5 +154,3 @@ class LeaderboardStore { return Math.floor(date.setHours(date.getHours() - range) / 1000); }; } - -export default LeaderboardStore; diff --git a/src/stores/MarketStore.ts b/src/stores/MarketStore.ts new file mode 100644 index 00000000..138d4ce8 --- /dev/null +++ b/src/stores/MarketStore.ts @@ -0,0 +1,186 @@ +import { makeAutoObservable, reaction } from "mobx"; +import { Nullable, Undefinable } from "tsdef"; + +import { DEFAULT_MARKET } from "@constants"; +import BN from "@utils/BN"; +import { CONFIG } from "@utils/getConfig"; +import { IntervalUpdater } from "@utils/IntervalUpdater"; + +import { FuelNetwork } from "@blockchain"; +import { PerpMarket, SpotMarket } from "@entity"; +import { BaseMarket } from "@entity/BaseMarket"; + +import RootStore from "./RootStore"; + +export interface ISerializedMarketStore { + favMarkets: Nullable; +} + +const MARKET_PRICES_UPDATE_INTERVAL = 5 * 1000; // 5 sec + +export class MarketStore { + private readonly rootStore: RootStore; + + favMarkets: string[] = []; + private setFavMarkets = (v: string[]) => (this.favMarkets = v); + + markets: BaseMarket[] = []; + private setMarkets = (markets: BaseMarket[]) => (this.markets = markets); + + marketSelectionOpened = false; + setMarketSelectionOpened = (s: boolean) => (this.marketSelectionOpened = s); + + marketSymbol = DEFAULT_MARKET; + setMarketSymbol = (v: string) => (this.marketSymbol = v); + + private marketPricesUpdater: IntervalUpdater; + + constructor(rootStore: RootStore, initState?: ISerializedMarketStore) { + this.rootStore = rootStore; + makeAutoObservable(this); + + const { oracleStore } = rootStore; + + if (initState) { + const favMarkets = initState.favMarkets?.split(",").filter(Boolean); + favMarkets && this.setFavMarkets(favMarkets); + } + + this.initMarkets(); + + this.marketPricesUpdater = new IntervalUpdater(this.updateMarketPrices, MARKET_PRICES_UPDATE_INTERVAL); + + reaction( + () => [this.market, oracleStore.initialized], + () => { + this.updateMarketPrices(); + }, + { fireImmediately: true }, + ); + + this.marketPricesUpdater.run(); + } + + get market() { + return this.markets.find((market) => market.symbol === this.marketSymbol); + } + + get spotMarket(): Undefinable { + if (!this.market) return; + + if (SpotMarket.isInstance(this.market)) { + return this.market; + } + } + + get perpMarket(): Undefinable { + if (!this.market) return; + + if (PerpMarket.isInstance(this.market)) { + return this.market; + } + } + + get initialized() { + return true; + } + + selectActiveMarket = (marketId?: string) => { + const bcNetwork = FuelNetwork.getInstance(); + + if (!marketId || marketId === this.marketSymbol) return; + + const selectedMarket = this.markets.find((market) => market.symbol === marketId); + if (!selectedMarket) return; + + if (SpotMarket.isInstance(selectedMarket)) { + const spotIndexerInfo = CONFIG.SPOT.INDEXERS[selectedMarket.contractAddress as keyof typeof CONFIG.SPOT.INDEXERS]; + bcNetwork.setSpotActiveMarket(selectedMarket.contractAddress, spotIndexerInfo); + } else if (PerpMarket.isInstance(selectedMarket)) { + const perpIndexerInfo = CONFIG.PERP.INDEXERS[selectedMarket.contractAddress as keyof typeof CONFIG.PERP.INDEXERS]; + bcNetwork.setPerpActiveMarket(perpIndexerInfo); + } else { + throw new Error("Market type not supported"); + } + + this.setMarketSymbol(marketId); + }; + + toggleFavMarket = (marketId: string) => { + const isFav = this.favMarkets.includes(marketId); + + if (isFav) { + this.setFavMarkets(this.favMarkets.filter((id) => id !== marketId)); + } else { + this.setFavMarkets([...this.favMarkets, marketId]); + } + }; + + updateMarketPrices = async () => { + const { oracleStore } = this.rootStore; + + this.markets.forEach((market) => { + let price = BN.ZERO; + + if (SpotMarket.isInstance(market)) { + // TODO: Fix logic for spot + price = BN.ZERO; + } else if (PerpMarket.isInstance(market)) { + price = market.baseToken.priceFeed + ? new BN(oracleStore.getTokenIndexPrice(market.baseToken.priceFeed)) + : BN.ZERO; + } + + market.setPrice(price); + }); + }; + + addToFav = (marketId: string) => { + if (!this.favMarkets.includes(marketId)) { + this.setFavMarkets([...this.favMarkets, marketId]); + } + }; + + removeFromFav = (marketId: string) => { + const index = this.favMarkets.indexOf(marketId); + index !== -1 && this.favMarkets.splice(index, 1); + }; + + private initMarkets = async () => { + try { + const spotMarkets = CONFIG.SPOT.MARKETS.map( + (market) => + new SpotMarket({ + baseTokenAddress: market.baseAssetId, + quoteTokenAddress: market.quoteAssetId, + contractAddress: market.contractId, + }), + ); + + const perpMarkets = CONFIG.PERP.MARKETS.map( + (market) => + new PerpMarket({ + baseTokenAddress: market.baseAssetId, + quoteTokenAddress: market.quoteAssetId, + contractAddress: market.contractId, + imRatio: BN.ZERO, + mmRatio: BN.ZERO, + status: "Opened", + }), + ); + + const allMarkets = [...spotMarkets, ...perpMarkets]; + + const market = allMarkets[0]; + + this.setMarkets(allMarkets); + this.selectActiveMarket(market.symbol); + } catch (error) { + console.error("Error init spot market", error); + } + }; + + serialize = (): ISerializedMarketStore => ({ + favMarkets: this.favMarkets.join(","), + }); +} diff --git a/src/stores/MixPanelStore.ts b/src/stores/MixPanelStore.ts index a77f5dc6..260ac85f 100644 --- a/src/stores/MixPanelStore.ts +++ b/src/stores/MixPanelStore.ts @@ -2,14 +2,14 @@ import mixpanel, { Mixpanel } from "mixpanel-browser"; import { makeAutoObservable, reaction } from "mobx"; import { Nullable } from "tsdef"; -import RootStore from "@stores/RootStore"; - import { CONFIG } from "@utils/getConfig"; +import RootStore from "./RootStore"; + const MAINNET_KEY = "1753ab2fe514a08e22df236ff4095905"; const TESTNET_KEY = "126ffbcd33aa8abbf4f91bea25e70cc4"; -class MixPanelStore { +export class MixPanelStore { private readonly rootStore: RootStore; mixpanel: Nullable = null; @@ -55,12 +55,12 @@ class MixPanelStore { } } -export default MixPanelStore; - export enum MIXPANEL_EVENTS { CLICK_DASHBOARD = "click_dashboard", CLICK_SPOT = "click_spot", CLICK_MAX_SPOT = "click_max_spot", + CLICK_PERP = "click_perp", + CLICK_MAX_PERP = "click_max_perp", AGREE_WITH_TERMS = "agree_with_terms", WALLET_CONNECTED = "wallet_connected", diff --git a/src/stores/ModalStore.ts b/src/stores/ModalStore.ts index a4f5437d..756b9de9 100644 --- a/src/stores/ModalStore.ts +++ b/src/stores/ModalStore.ts @@ -4,7 +4,7 @@ import { Nullable } from "tsdef"; import RootStore from "./RootStore"; export enum MODAL_TYPE { - DEPOSIT_WITHDRAW_MODAL, + PERP_DEPOSIT_WITHDRAW_MODAL, CONNECT_MODAL, } diff --git a/src/stores/NotificationStore.ts b/src/stores/NotificationStore.ts index e2e0a60d..7ec0a980 100644 --- a/src/stores/NotificationStore.ts +++ b/src/stores/NotificationStore.ts @@ -3,15 +3,15 @@ import { makeAutoObservable } from "mobx"; import { createToast, NotificationProps } from "@components/Toast"; -import RootStore from "@stores/RootStore"; - import { getDeviceInfo } from "@utils/getDeviceInfo"; +import RootStore from "./RootStore"; + type NotificationParams = Omit & { options?: ToastOptions; }; -class NotificationStore { +export class NotificationStore { private readonly rootStore: RootStore; constructor(rootStore: RootStore) { @@ -59,5 +59,3 @@ class NotificationStore { this.notify(params, "info"); }; } - -export default NotificationStore; diff --git a/src/stores/OracleStore.ts b/src/stores/OracleStore.ts index cad0f445..944ef518 100644 --- a/src/stores/OracleStore.ts +++ b/src/stores/OracleStore.ts @@ -1,20 +1,20 @@ import { HermesClient } from "@pythnetwork/hermes-client"; import { makeAutoObservable } from "mobx"; -import RootStore from "@stores/RootStore"; - import BN from "@utils/BN"; import { IntervalUpdater } from "@utils/IntervalUpdater"; import { FuelNetwork } from "@blockchain"; +import RootStore from "./RootStore"; + const PYTH_URL = "https://hermes.pyth.network"; const zeroFeedId = "0x0000000000000000000000000000000000000000000000000000000000000000"; const UPDATE_INTERVAL = 15_000; -class OracleStore { +export class OracleStore { private readonly rootStore: RootStore; priceServiceConnection: HermesClient; @@ -35,8 +35,8 @@ class OracleStore { } get tokenIndexPrice(): BN { - const { market } = this.rootStore.tradeStore; - const token = market?.baseToken; + const { marketStore } = this.rootStore; + const token = marketStore.market?.baseToken; if (!token) return BN.ZERO; @@ -84,5 +84,3 @@ class OracleStore { private setInitialized = (l: boolean) => (this.initialized = l); } - -export default OracleStore; diff --git a/src/stores/PerpCreateOrderStore.ts b/src/stores/PerpCreateOrderStore.ts new file mode 100644 index 00000000..26244ba2 --- /dev/null +++ b/src/stores/PerpCreateOrderStore.ts @@ -0,0 +1,320 @@ +import _ from "lodash"; +import { makeAutoObservable, reaction } from "mobx"; + +import { LimitType } from "@compolabs/spark-orderbook-ts-sdk"; + +import { RootStore } from "@stores"; +import { MIXPANEL_EVENTS } from "@stores/MixPanelStore"; + +import { DEFAULT_DECIMALS } from "@constants"; +import BN from "@utils/BN"; +import { ACTION_MESSAGE_TYPE, getActionMessage } from "@utils/getActionMessage"; +import { CONFIG } from "@utils/getConfig"; +import { handleWalletErrors } from "@utils/handleWalletErrors"; +import Math from "@utils/Math"; + +import { FuelNetwork } from "@blockchain"; +import { PerpMarketOrder } from "@entity"; + +const PRICE_UPDATE_THROTTLE_INTERVAL = 1000; // 1s + +export enum ORDER_MODE { + BUY, + SELL, +} + +export enum ACTIVE_INPUT { + Price, + Amount, + Total, +} + +export enum ORDER_TYPE { + Market, + Limit, + StopMarket, + StopLimit, + TakeProfit, + TakeProfitLimit, + LimitFOK, + LimitIOC, // TODO: Когда TimeInForce будет переделана в отдельную вкладку оставить только тип limit +} + +export class PerpCreateOrderStore { + isLoading = false; + + mode = ORDER_MODE.BUY; + + activeInput = ACTIVE_INPUT.Amount; + + inputPrice = BN.ZERO; + slippage = new BN(10); + inputAmount = BN.ZERO; + inputPercent = BN.ZERO; + inputTotal = BN.ZERO; + + inputLeverage = BN.ZERO; + inputLeveragePercent = BN.ZERO; + + constructor(private rootStore: RootStore) { + makeAutoObservable(this); + + const { perpOrderBookStore, marketStore, settingsStore } = this.rootStore; + + // TODO: Fix the bug where the price doesn’t change when switching markets + reaction( + () => [perpOrderBookStore.allBuyOrders, perpOrderBookStore.allSellOrders], + ([buyOrders, sellOrders]) => { + const orders = this.isSell ? buyOrders : sellOrders; + const order = orders[orders.length - 1]; + + if (!order) return; + + const shouldSetMarketPrice = settingsStore.orderType === ORDER_TYPE.Market; + const shouldSetDefaultLimitPrice = + settingsStore.orderType === ORDER_TYPE.Limit && + this.inputPrice.isZero() && + this.activeInput !== ACTIVE_INPUT.Price; + + if (shouldSetMarketPrice || shouldSetDefaultLimitPrice) { + this.setInputPriceThrottle(order.price); + } + }, + ); + + reaction( + () => [this.isSell, settingsStore.orderType], + ([isSell]) => { + const orders = isSell ? perpOrderBookStore.allBuyOrders : perpOrderBookStore.allSellOrders; + const order = orders[orders.length - 1]; + + if (!order) return; + + this.setInputPriceThrottle(order.price); + }, + ); + + reaction( + () => marketStore.perpMarket, + () => { + this.setInputAmount(BN.ZERO); + this.setInputTotal(BN.ZERO); + this.setInputPercent(0); + }, + ); + } + + get canProceed() { + return ( + this.inputAmount.gt(BN.ZERO) && this.inputPrice.gt(BN.ZERO) && this.inputTotal.gt(BN.ZERO) && !this.isInputError + ); + } + + get isInputError(): boolean { + const { balanceStore, marketStore } = this.rootStore; + + if (!marketStore.perpMarket) return false; + + const amount = this.isSell ? this.inputAmount : this.inputTotal; + const token = this.isSell ? marketStore.perpMarket.baseToken : marketStore.perpMarket.quoteToken; + + const totalBalance = balanceStore.getPerpTotalBalance(token.assetId); + + return totalBalance ? amount.gt(totalBalance) : false; + } + + get isSell(): boolean { + return this.mode === ORDER_MODE.SELL; + } + + setActiveInput = (input?: ACTIVE_INPUT) => (this.activeInput = input === undefined ? ACTIVE_INPUT.Amount : input); + + setOrderMode = (mode: ORDER_MODE) => { + this.mode = mode; + + this.setInputAmount(BN.ZERO); + this.setInputTotal(BN.ZERO); + this.setInputPercent(0); + }; + + onMaxClick = () => { + const { mixPanelStore, balanceStore, marketStore } = this.rootStore; + + if (!marketStore.perpMarket) return; + + const token = this.isSell ? marketStore.perpMarket.baseToken : marketStore.perpMarket.quoteToken; + + const totalBalance = balanceStore.getPerpTotalBalance(token.assetId); + if (this.isSell) { + this.setInputAmount(totalBalance); + return; + } + + mixPanelStore.trackEvent(MIXPANEL_EVENTS.CLICK_MAX_PERP, { + type: this.isSell ? "SELL" : "BUY", + value: totalBalance.toString(), + }); + + this.setInputTotal(totalBalance); + }; + + setInputPrice = (price: BN) => { + const { settingsStore } = this.rootStore; + this.inputPrice = price; + if (settingsStore.orderType !== ORDER_TYPE.Market) this.setActiveInput(ACTIVE_INPUT.Price); + this.calculateInputs(); + }; + + setInputSlippage = (slippage: BN) => { + this.slippage = slippage; + }; + + setInputAmount = (amount: BN) => { + this.inputAmount = amount; + this.setActiveInput(ACTIVE_INPUT.Amount); + this.calculateInputs(); + }; + + setInputTotal = (total: BN) => { + this.inputTotal = total; + this.setActiveInput(ACTIVE_INPUT.Total); + this.calculateInputs(); + }; + + private calculateInputs(): void { + const { marketStore, perpMarketInfoStore } = this.rootStore; + if (!marketStore.perpMarket) return; + + const baseDecimals = marketStore.perpMarket.baseToken.decimals; + const quoteDecimals = marketStore.perpMarket.quoteToken.decimals; + + const newInputTotal = Math.multiplyWithDifferentDecimals( + this.inputAmount, + baseDecimals, + this.inputPrice, + DEFAULT_DECIMALS, + quoteDecimals, + ); + + const newInputAmount = Math.divideWithDifferentDecimals( + this.inputTotal, + quoteDecimals, + this.inputPrice, + DEFAULT_DECIMALS, + baseDecimals, + ); + + switch (this.activeInput) { + case ACTIVE_INPUT.Price: + case ACTIVE_INPUT.Amount: + this.inputTotal = newInputTotal; + break; + case ACTIVE_INPUT.Total: + this.inputAmount = newInputAmount; + break; + } + + this.updatePercent(); + + perpMarketInfoStore.fetchTradeFeeDebounce(this.inputTotal.toString()); + } + + private updatePercent(): void { + const { balanceStore, marketStore } = this.rootStore; + + if (!marketStore.perpMarket) return; + + const token = this.isSell ? marketStore.perpMarket.baseToken : marketStore.perpMarket.quoteToken; + + const totalBalance = balanceStore.getPerpTotalBalance(token.assetId); + + if (totalBalance.isZero()) { + this.inputPercent = BN.ZERO; + return; + } + + const percentageOfTotal = this.isSell + ? BN.ratioOf(this.inputAmount, totalBalance) + : BN.ratioOf(this.inputTotal, totalBalance); + + this.inputPercent = percentageOfTotal.gt(100) ? new BN(100) : percentageOfTotal.toDecimalPlaces(0); + } + + setInputPriceThrottle = _.throttle(this.setInputPrice, PRICE_UPDATE_THROTTLE_INTERVAL); + + setInputPercent = (value: number | number[]) => (this.inputPercent = new BN(value.toString())); + + createOrder = async () => { + const { marketStore, notificationStore, balanceStore, mixPanelStore, settingsStore, accountStore } = this.rootStore; + + if (!marketStore.perpMarket) return; + + this.isLoading = true; + + const isBuy = this.mode === ORDER_MODE.BUY; + + const bcNetwork = FuelNetwork.getInstance(); + + const clearingHouseContract = bcNetwork.perpetualSdk.getClearingHouseContract(CONFIG.PERP.CONTRACTS.clearingHouse); + const pythContract = bcNetwork.perpetualSdk.getPythContract(CONFIG.PERP.CONTRACTS.pyth); + + if (bcNetwork.getIsExternalWallet()) { + notificationStore.info({ text: "Please, confirm operation in your wallet" }); + } + + // const token = isBuy ? marketStore.perpMarket.baseToken : marketStore.perpMarket.quoteToken; + const token = marketStore.perpMarket.baseToken; + const amount = this.inputAmount; + const price = this.inputPrice; + + const totalAmount = isBuy ? amount : amount.negated(); + console.log("price", price.toString(), totalAmount.toString()); + + try { + const hash = await clearingHouseContract.openOrderC( + token.assetId, + token.priceFeed, + totalAmount, + price, + pythContract, + ); + + this.setInputTotal(BN.ZERO); + + mixPanelStore.trackEvent(MIXPANEL_EVENTS.CONFIRM_ORDER, { + order_type: isBuy ? "BUY" : "SELL", + token_1: marketStore.perpMarket.baseToken.symbol, + token_2: marketStore.perpMarket.quoteToken.symbol, + transaction_sum: BN.formatUnits(amount, token.decimals).toSignificant(2), + user_address: accountStore.address, + }); + + notificationStore.success({ + text: getActionMessage(ACTION_MESSAGE_TYPE.CREATING_ORDER)( + BN.formatUnits(amount, token.decimals).toSignificant(2), + token.symbol, + BN.formatUnits(this.inputPrice, DEFAULT_DECIMALS).toSignificant(2), + isBuy ? "buy" : "sell", + ), + hash, + }); + } catch (error: any) { + const action = + settingsStore.orderType === ORDER_TYPE.Market + ? ACTION_MESSAGE_TYPE.CREATING_ORDER_FAILED + : ACTION_MESSAGE_TYPE.CREATING_ORDER_FAILED_INSTRUCTION; + handleWalletErrors(notificationStore, error, getActionMessage(action)()); + } + + await balanceStore.update(); + this.isLoading = false; + }; + + selectOrderbookOrder = async (order: PerpMarketOrder, mode: ORDER_MODE) => { + const { settingsStore } = this.rootStore; + settingsStore.setTimeInForce(LimitType.GTC); + settingsStore.setOrderType(ORDER_TYPE.Limit); + this.setOrderMode(mode); + this.setInputPrice(order.price); + }; +} diff --git a/src/stores/PerpMarketInfoStore.ts b/src/stores/PerpMarketInfoStore.ts new file mode 100644 index 00000000..832bcfd1 --- /dev/null +++ b/src/stores/PerpMarketInfoStore.ts @@ -0,0 +1,162 @@ +import { toBech32 } from "fuels"; +import _ from "lodash"; +import { makeAutoObservable, reaction } from "mobx"; + +import { DEFAULT_DECIMALS } from "@constants"; +import BN from "@utils/BN"; +import { IntervalUpdater } from "@utils/IntervalUpdater"; + +import { FuelNetwork } from "@blockchain"; + +import RootStore from "./RootStore"; + +const MARKET_INFO_UPDATE_INTERVAL = 5 * 60 * 1000; // 5 min + +export class PerpMarketInfoStore { + private readonly rootStore: RootStore; + + marketInfo = { + volume: BN.ZERO, + high: BN.ZERO, + low: BN.ZERO, + }; + + matcherFee = BN.ZERO; + tradeFee = { + makerFee: BN.ZERO, + takerFee: BN.ZERO, + }; + + minimalOrder = { + minPrice: BN.ZERO, + minOrder: BN.ZERO, + }; + + isTradeFeeLoading = false; + isMatcherFeeLoading = false; + + private marketInfoUpdater: IntervalUpdater; + + constructor(rootStore: RootStore) { + this.rootStore = rootStore; + makeAutoObservable(this); + + const { oracleStore, marketStore } = rootStore; + + this.marketInfoUpdater = new IntervalUpdater(this.updateMarketInfo, MARKET_INFO_UPDATE_INTERVAL); + + reaction( + () => [marketStore.market, oracleStore.initialized], + () => { + this.updateMarketInfo(); + this.fetchMatcherFee(); + this.getMinimalOrder(); + }, + { fireImmediately: true }, + ); + + this.marketInfoUpdater.run(true); + } + + get isFeeLoading(): boolean { + return this.isTradeFeeLoading || this.isMatcherFeeLoading; + } + + get exchangeFee(): BN { + const { makerFee, takerFee } = this.tradeFee; + + return BN.max(makerFee, takerFee); + } + + get exchangeFeeFormat(): BN { + const { marketStore } = this.rootStore; + if (!marketStore.perpMarket) return BN.ZERO; + + const decimals = marketStore.perpMarket.quoteToken.decimals; + return BN.formatUnits(this.exchangeFee, decimals); + } + + get matcherFeeFormat(): BN { + const { marketStore } = this.rootStore; + if (!marketStore.perpMarket) return BN.ZERO; + + const decimals = marketStore.perpMarket.quoteToken.decimals; + return BN.formatUnits(this.matcherFee, decimals); + } + + getIsEnoughtMoneyForFee(isSell: boolean) { + const { marketStore } = this.rootStore; + + if (!marketStore.perpMarket || isSell) return true; + const { balanceStore } = this.rootStore; + + const walletAmount = balanceStore.getWalletBalance(marketStore.perpMarket.quoteToken.assetId); + + return this.exchangeFee.plus(this.matcherFee).lte(walletAmount); + } + + getMinimalOrder = async () => { + const bcNetwork = FuelNetwork.getInstance(); + const [order, price] = await Promise.all([bcNetwork.spotFetchMinOrderSize(), bcNetwork.spotFetchMinOrderPrice()]); + this.minimalOrder = { + minPrice: new BN(price), + minOrder: new BN(order), + }; + }; + + updateMarketInfo = async () => { + const { marketStore } = this.rootStore; + if (!marketStore.perpMarket) return; + + const info = await FuelNetwork.getInstance().spotFetchVolume({ + limit: 1000, + market: [marketStore.perpMarket.contractAddress], + }); + + const volume = BN.formatUnits(info.volume, marketStore.perpMarket.baseToken.decimals); + const low = BN.formatUnits(info.low, DEFAULT_DECIMALS); + const high = BN.formatUnits(info.high, DEFAULT_DECIMALS); + + this.marketInfo = { + volume, + low, + high, + }; + }; + + fetchMatcherFee = async () => { + const { marketStore } = this.rootStore; + const bcNetwork = FuelNetwork.getInstance(); + + if (!marketStore.perpMarket) return; + + this.isMatcherFeeLoading = true; + const matcherFee = await bcNetwork.spotFetchMatcherFee(); + + this.matcherFee = new BN(matcherFee); + this.isMatcherFeeLoading = false; + }; + + private fetchTradeFee = async (quoteAmount: string) => { + const { accountStore, marketStore } = this.rootStore; + + if (new BN(quoteAmount).isZero()) { + this.tradeFee = { makerFee: BN.ZERO, takerFee: BN.ZERO }; + return; + } + + const bcNetwork = FuelNetwork.getInstance(); + + if (!accountStore.address || !marketStore.perpMarket) return; + + this.isTradeFeeLoading = true; + const address = toBech32(accountStore.address!); + + const { makerFee, takerFee } = await bcNetwork.spotFetchProtocolFeeAmountForUser(quoteAmount, address); + + this.tradeFee = { makerFee: new BN(makerFee), takerFee: new BN(takerFee) }; + this.isTradeFeeLoading = false; + }; + + fetchTradeFeeDebounce = _.debounce(this.fetchTradeFee, 250); +} diff --git a/src/stores/PerpOrderBookStore.ts b/src/stores/PerpOrderBookStore.ts new file mode 100644 index 00000000..f5a15768 --- /dev/null +++ b/src/stores/PerpOrderBookStore.ts @@ -0,0 +1,272 @@ +import { HistogramData } from "lightweight-charts"; +import { makeAutoObservable, reaction } from "mobx"; +import { Nullable } from "tsdef"; + +import { GetActiveOrdersParams, OrderType } from "@compolabs/spark-orderbook-ts-sdk"; + +import { RootStore } from "@stores"; + +import { SPOT_ORDER_FILTER } from "@screens/PerpScreen/OrderbookAndTrades/PerpOrderBook/PerpOrderBook.tsx"; + +import { DEFAULT_DECIMALS } from "@constants"; +import BN from "@utils/BN"; +import { OhlcvData } from "@utils/getOhlcvData"; +import { groupOrders } from "@utils/groupOrders"; + +import { FuelNetwork } from "@blockchain"; +import { PerpMarketOrder, PerpMarketTrade } from "@entity"; + +import { Subscription } from "@src/typings/utils"; + +export class PerpOrderBookStore { + private readonly rootStore: RootStore; + private subscriptionToTradeOrderEvents: Nullable = null; + + trades: PerpMarketTrade[] = []; + isInitialLoadComplete = false; + + allBuyOrders: PerpMarketOrder[] = []; + allSellOrders: PerpMarketOrder[] = []; + + decimalGroup = 2; + orderFilter: SPOT_ORDER_FILTER = SPOT_ORDER_FILTER.SELL_AND_BUY; + + isOrderBookLoading = true; + + private buySubscription: Nullable = null; + private sellSubscription: Nullable = null; + + ohlcvData: OhlcvData[] = []; + historgramData: HistogramData[] = []; + + constructor(rootStore: RootStore) { + this.rootStore = rootStore; + makeAutoObservable(this); + + const { initialized, marketStore } = this.rootStore; + + reaction( + () => [initialized, marketStore.perpMarket], + ([market, initialized]) => { + if (!initialized || !market) return; + + this.updateOrderBook(); + this.subscribeTrades(); + }, + { fireImmediately: true }, + ); + } + + private _sortOrders(orders: PerpMarketOrder[], reverse: boolean): PerpMarketOrder[] { + return orders.sort((a, b) => { + if (reverse) { + return a.price.lt(b.price) ? -1 : 1; + } else { + return a.price.lt(b.price) ? 1 : -1; + } + }); + } + + private _getOrders(orders: PerpMarketOrder[], reverse = false): PerpMarketOrder[] { + const groupedOrders = groupOrders(orders, this.decimalGroup); + return this._sortOrders(groupedOrders.slice(), reverse); + } + + get buyOrders(): PerpMarketOrder[] { + return this._getOrders(this.allBuyOrders, true); + } + + get sellOrders(): PerpMarketOrder[] { + return this._getOrders(this.allSellOrders); + } + + get totalBuy(): BN { + return this.buyOrders.reduce((acc, order) => acc.plus(order.initialQuoteAmount), BN.ZERO); + } + + get totalSell(): BN { + return this.sellOrders.reduce((acc, order) => acc.plus(order.initialAmount), BN.ZERO); + } + + get lastTradePrice(): BN { + const { oracleStore, marketStore } = this.rootStore; + + if (!marketStore.perpMarket) { + return BN.ZERO; + } + + return oracleStore.getTokenIndexPrice(marketStore.perpMarket.baseToken.priceFeed); + + // if (!this.trades.length) { + // return BN.ZERO; + // } + + // return new BN(this.trades[0].tradePrice); + } + + setDecimalGroup = (value: number) => { + this.decimalGroup = value; + }; + + setOrderFilter = (value: SPOT_ORDER_FILTER) => (this.orderFilter = value); + + updateOrderBook = () => { + const { marketStore } = this.rootStore; + const market = marketStore.perpMarket; + + if (!this.rootStore.initialized || !market) return; + + const bcNetwork = FuelNetwork.getInstance(); + + const params: Omit = { + limit: 150, + market: [market.contractAddress], + }; + + this.subscribeToBuyOrders(bcNetwork, params); + this.subscribeToSellOrders(bcNetwork, params); + }; + + private subscribeToOrders( + orderType: OrderType, + subscription: Subscription | null, + updateOrders: (orders: PerpMarketOrder[]) => void, + bcNetwork: FuelNetwork, + params: Omit, + ) { + if (subscription) { + subscription.unsubscribe(); + } + + const { marketStore } = this.rootStore; + const market = marketStore.perpMarket; + const newSubscription = bcNetwork.perpSubscribeActiveOrders({ ...params, orderType }).subscribe({ + next: ({ data }) => { + this.isOrderBookLoading = false; + if (!data) return; + const orders = ("ActiveBuyOrder" in data ? data.ActiveBuyOrder : data.ActiveSellOrder).map( + (order) => + new PerpMarketOrder({ + ...order, + quoteAssetId: market!.quoteToken.assetId, + }), + ); + updateOrders(orders); + }, + }); + if (orderType === OrderType.Buy) { + this.buySubscription = newSubscription; + } else { + this.sellSubscription = newSubscription; + } + } + + private subscribeToBuyOrders(bcNetwork: FuelNetwork, params: Omit) { + this.subscribeToOrders( + OrderType.Buy, + this.buySubscription, + (orders) => (this.allBuyOrders = orders), + bcNetwork, + params, + ); + } + + private subscribeToSellOrders(bcNetwork: FuelNetwork, params: Omit) { + this.subscribeToOrders( + OrderType.Sell, + this.sellSubscription, + (orders) => { + this.allSellOrders = orders; + }, + bcNetwork, + params, + ); + } + + private getPrice(orders: PerpMarketOrder[], priceType: "max" | "min"): BN { + const compareType = priceType === "max" ? "gt" : "lt"; + return orders.reduce( + (value, order) => (order.price[compareType](value) ? order.price : value), + orders[0]?.price ?? BN.ZERO, + ); + } + + private get getMaxBuyPrice(): BN { + return this.getPrice(this.allBuyOrders, "max"); + } + + private get getMinSellPrice(): BN { + return this.getPrice(this.allSellOrders, "min"); + } + + get spread(): BN { + return this.getMinSellPrice.minus(this.getMaxBuyPrice); + } + + get isSpreadValid(): boolean { + if (this.getMinSellPrice.isZero() || this.getMaxBuyPrice.isZero()) { + return false; + } + + return true; + } + + get spreadPrice(): string { + return BN.formatUnits(this.spread, DEFAULT_DECIMALS).toSignificant(2); + } + + get spreadPercent(): string { + return BN.ratioOf(this.spread, this.getMaxBuyPrice).toFormat(2); + } + + subscribeTrades = () => { + const { marketStore } = this.rootStore; + const market = marketStore.perpMarket; + + const bcNetwork = FuelNetwork.getInstance(); + + if (this.subscriptionToTradeOrderEvents) { + this.subscriptionToTradeOrderEvents.unsubscribe(); + } + console.log("!"); + try { + this.subscriptionToTradeOrderEvents = bcNetwork + .perpSubscribeTradeOrderEvents({ + limit: 500, + // market: [market!.contractAddress], + }) + .subscribe({ + next: ({ data }) => { + if (!data) return; + console.log("data", data); + // TODO implement perp logic + const trades = data.TradeEvent.map( + (trade) => + new PerpMarketTrade({ + ...trade, + baseAssetId: market!.baseToken.assetId, + quoteAssetId: market!.quoteToken.assetId, + }), + ); + this.trades = trades; + + // const ohlcvData = getOhlcvData(data.TradeOrderEvent, "1m"); + // this.ohlcvData = ohlcvData.ohlcvData; + // this.historgramData = ohlcvData.historgramData; + + if (!this.isInitialLoadComplete) { + this.isInitialLoadComplete = true; + } + }, + }); + } catch (err) { + console.log("err", err); + } + }; + + get isTradesLoading() { + return !this.isInitialLoadComplete; + } +} + +export default PerpOrderBookStore; diff --git a/src/stores/PerpTableStore.ts b/src/stores/PerpTableStore.ts new file mode 100644 index 00000000..97a934ba --- /dev/null +++ b/src/stores/PerpTableStore.ts @@ -0,0 +1,299 @@ +import { makeAutoObservable, reaction } from "mobx"; +import { Nullable } from "tsdef"; + +import { BN, OrderType, UserInfo } from "@compolabs/spark-orderbook-ts-sdk"; +import { PerpOrder } from "@compolabs/spark-perpetual-ts-sdk"; + +import { RootStore } from "@stores"; + +import { ACTION_MESSAGE_TYPE, getActionMessage } from "@utils/getActionMessage"; +import { CONFIG } from "@utils/getConfig.ts"; +import { handleWalletErrors } from "@utils/handleWalletErrors"; + +import { FuelNetwork } from "@blockchain"; +import { PerpMarket, PerpMarketOrder, PerpPosition } from "@entity"; + +import { Subscription } from "@src/typings/utils"; + +const sortDesc = (a: PerpMarketOrder, b: PerpMarketOrder) => b.timestamp.valueOf() - a.timestamp.valueOf(); + +export const PAGINATION_LIMIT = 10; + +export class PerpTableStore { + private readonly rootStore: RootStore; + private subscriptionToOpenOrders: Nullable = null; + private subscriptionToHistoryOrders: Nullable = null; + private subscriptionToOrdersStats: Nullable = null; + private subscriptionToOpenPosition: Nullable = null; + + userOrders: PerpMarketOrder[] = []; + private setUserOrders = (orders: PerpMarketOrder[]) => (this.userOrders = orders); + + userOrdersHistory: PerpMarketOrder[] = []; + private setUserOrdersHistory = (orders: PerpMarketOrder[]) => (this.userOrdersHistory = orders); + + userOrdersStats: Nullable = null; + private setUserOrdersStats = (stats: UserInfo) => (this.userOrdersStats = stats); + + userOpenPosition: any[] = []; + private setUserOpenPosition = (stats: any) => (this.userOpenPosition = stats); + + isOrderCancelling = false; + cancelingOrderId: Nullable = null; + isWithdrawing = false; + withdrawingAssetId: Nullable = null; + + isOpenOrdersLoaded = false; + isHistoryOrdersLoaded = false; + + offset = 0; + setOffset = (currentPage: number) => { + if (currentPage === 0) { + this.offset = 0; + return; + } + + this.offset = (currentPage - 1) * PAGINATION_LIMIT; + }; + + filterIsSellOrderTypeEnabled = true; + filterIsBuyOrderTypeEnabled = true; + toggleFilterOrderType = (orderType: OrderType) => { + if (orderType === OrderType.Sell) { + if (this.filterIsSellOrderTypeEnabled && !this.filterIsBuyOrderTypeEnabled) { + this.filterIsSellOrderTypeEnabled = false; + this.filterIsBuyOrderTypeEnabled = true; + return; + } + this.filterIsSellOrderTypeEnabled = !this.filterIsSellOrderTypeEnabled; + return; + } + + if (this.filterIsBuyOrderTypeEnabled && !this.filterIsSellOrderTypeEnabled) { + // Cannot uncheck 'buy' because 'sell' is already unchecked + this.filterIsBuyOrderTypeEnabled = false; + this.filterIsSellOrderTypeEnabled = true; + return; + } + this.filterIsBuyOrderTypeEnabled = !this.filterIsBuyOrderTypeEnabled; + }; + + constructor(rootStore: RootStore) { + makeAutoObservable(this); + this.rootStore = rootStore; + const { accountStore, marketStore } = this.rootStore; + reaction( + () => + [ + marketStore.market, + this.rootStore.initialized, + accountStore.isConnected, + this.tableFilters, + FuelNetwork.getInstance().getWallet(), + ] as const, + ([market, initialized, isConnected, _, instanceWallet]) => { + const isPerpMarket = market && PerpMarket.isInstance(market); + console.log("instanceWallet", instanceWallet); + if (!isPerpMarket || !initialized || !isConnected || !instanceWallet) { + this.setUserOrders([]); + this.setUserOrdersHistory([]); + return; + } + + this.subscribeToOrders(); + }, + { fireImmediately: true }, + ); + } + + get initialized() { + return this.isOpenOrdersLoaded && this.isHistoryOrdersLoaded; + } + + get tableFilters() { + const orderType = + this.filterIsSellOrderTypeEnabled && this.filterIsBuyOrderTypeEnabled + ? undefined + : this.filterIsSellOrderTypeEnabled + ? OrderType.Sell + : OrderType.Buy; + + return { + limit: PAGINATION_LIMIT, + offset: this.offset, + orderType, + }; + } + + resetCounter = () => { + this.userOrdersStats = null; + }; + + cancelOrder = async (order: PerpMarketOrder) => { + const { notificationStore, marketStore } = this.rootStore; + const bcNetwork = FuelNetwork.getInstance(); + + if (!marketStore.market) return; + + this.isOrderCancelling = true; + this.cancelingOrderId = order.id; + if (bcNetwork?.getIsExternalWallet()) { + notificationStore.info({ + text: "Please, confirm operation in your wallet", + }); + } + + try { + const bcNetworkCopy = await bcNetwork.spotChain(); + const tx = await bcNetworkCopy.writeWithMarket(order.market).cancelOrder(order.id); + notificationStore.success({ + text: getActionMessage(ACTION_MESSAGE_TYPE.CANCELING_ORDER)(), + hash: tx.transactionId, + }); + } catch (error: any) { + handleWalletErrors(notificationStore, error, getActionMessage(ACTION_MESSAGE_TYPE.CANCELING_ORDER_FAILED)()); + } + + this.isOrderCancelling = false; + this.cancelingOrderId = null; + }; + + withdrawBalance = async (assetId: string) => { + const { balanceStore } = this.rootStore; + + this.isWithdrawing = true; + this.withdrawingAssetId = assetId; + + const amount = balanceStore.getSpotContractBalance(assetId); + await balanceStore.withdrawSpotBalance(assetId, amount.toString()); + + this.isWithdrawing = false; + this.withdrawingAssetId = null; + }; + + private subscribeToOpenOrders = () => { + const { accountStore } = this.rootStore; + const bcNetwork = FuelNetwork.getInstance(); + + if (this.subscriptionToOpenOrders) { + this.subscriptionToOpenOrders.unsubscribe(); + } + + this.subscriptionToOpenOrders = bcNetwork + .perpSubscribeOrders({ + ...this.tableFilters, + trader: accountStore.address!, + status: ["Active"], + }) + .subscribe({ + next: ({ data }) => { + if (!data) return; + + const sortedOrder = data.Order.map((order) => new PerpMarketOrder(order as PerpOrder)).sort(sortDesc); + this.setUserOrders(sortedOrder); + + if (!this.isOpenOrdersLoaded) { + this.isOpenOrdersLoaded = true; + } + }, + }); + }; + + private subscribeToHistoryOrders = () => { + const { accountStore } = this.rootStore; + const bcNetwork = FuelNetwork.getInstance(); + + if (this.subscriptionToHistoryOrders) { + this.subscriptionToHistoryOrders.unsubscribe(); + } + this.subscriptionToHistoryOrders = bcNetwork + .perpSubscribeOrders({ + ...this.tableFilters, + trader: accountStore.address!, + status: ["Closed", "Canceled"], + }) + .subscribe({ + next: ({ data }) => { + if (!data) return; + + const sortedOrdersHistory = data.Order.map((order) => new PerpMarketOrder(order as PerpOrder)).sort(sortDesc); + this.setUserOrdersHistory(sortedOrdersHistory); + + if (!this.isHistoryOrdersLoaded) { + this.isHistoryOrdersLoaded = true; + } + }, + }); + }; + + private subscribeUserInfo = () => { + const bcNetwork = FuelNetwork.getInstance(); + const { accountStore } = this.rootStore; + + if (this.subscriptionToOrdersStats) { + this.subscriptionToOrdersStats.unsubscribe(); + } + + this.subscriptionToOrdersStats = bcNetwork + .spotSubscribeUserInfo({ + id: accountStore.address!, + }) + .subscribe({ + next: ({ data }: any) => { + if (!data.User.length) { + return; + } + this.setUserOrdersStats(data.User[0]); + }, + }); + }; + + private subscribeUserOpenPosition = async () => { + const bcNetwork = FuelNetwork.getInstance(); + const { accountStore, perpOrderBookStore } = this.rootStore; + if (this.subscriptionToOpenPosition) { + this.subscriptionToOpenPosition.unsubscribe(); + } + + const marketContract = bcNetwork.perpetualSdk.getPerpMarketContract(CONFIG.PERP.CONTRACTS.clearingHouse); + + const actualMarket = await marketContract.getMarketM( + "0x0dc8cdbe2798cb45ebc99180afc0bc514ffb505a80f122004378955c1d23892c", + ); + + this.subscriptionToOpenPosition = bcNetwork + .subscribeActivePositions({ + ...this.tableFilters, + trader: accountStore.address!, + }) + .subscribe({ + next: ({ data }: any) => { + if (!data?.AccountBalanceChangeEvent?.length) { + return; + } + try { + const markPrice = perpOrderBookStore.trades[0]?.tradePrice; + const sortedOrdersHistory = data?.AccountBalanceChangeEvent.filter( + (position: any) => + !new BN(position.takerOpenNotional).isZero() && !new BN(position.takerPositionSize).isZero(), + ) + .slice(0, 1) // Берем только первый элемент + .map( + (position: any) => + new PerpPosition({ ...position, imRatio: actualMarket?.imRatio, markPrice: markPrice }), + ); + this.setUserOpenPosition(sortedOrdersHistory); + } catch (err) { + console.log("err", err); + } + }, + }); + }; + + private subscribeToOrders = () => { + this.subscribeToOpenOrders(); + this.subscribeToHistoryOrders(); + this.subscribeUserInfo(); + this.subscribeUserOpenPosition(); + }; +} diff --git a/src/stores/QuickAssetsStore.ts b/src/stores/QuickAssetsStore.ts index f15b54d7..a407f6e1 100644 --- a/src/stores/QuickAssetsStore.ts +++ b/src/stores/QuickAssetsStore.ts @@ -1,12 +1,12 @@ import { makeAutoObservable } from "mobx"; -import RootStore from "@stores/RootStore"; +import RootStore from "./RootStore"; interface ISerializedQuickAssetsStore { quickAssets?: boolean; } -class QuickAssetsStore { +export class QuickAssetsStore { private readonly rootStore: RootStore; constructor(rootStore: RootStore, initState?: ISerializedQuickAssetsStore) { @@ -27,5 +27,3 @@ class QuickAssetsStore { quickAssets: this.openQuickAssets, }); } - -export default QuickAssetsStore; diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts index f9fdb640..0dd96e50 100644 --- a/src/stores/RootStore.ts +++ b/src/stores/RootStore.ts @@ -1,59 +1,91 @@ import { autorun, makeAutoObservable } from "mobx"; -import AccountStore, { ISerializedAccountStore } from "@stores/AccountStore"; -import DashboardStore from "@stores/DashboardStore"; -import FaucetStore from "@stores/FaucetStore"; -import LeaderboardStore from "@stores/LeaderboardStore.ts"; -import MixPanelStore from "@stores/MixPanelStore"; -import NotificationStore from "@stores/NotificationStore"; -import QuickAssetsStore from "@stores/QuickAssetsStore"; -import SettingsStore, { ISerializedSettingStore } from "@stores/SettingsStore"; -import TradeStore, { ISerializedTradeStore } from "@stores/TradeStore"; - import { saveState } from "@utils/localStorage"; +import { AccountStore, ISerializedAccountStore } from "./AccountStore"; import { BalanceStore } from "./BalanceStore"; +import { DashboardStore } from "./DashboardStore"; +import { FaucetStore } from "./FaucetStore"; +import { LeaderboardStore } from "./LeaderboardStore"; +import { ISerializedMarketStore, MarketStore } from "./MarketStore"; +import { MixPanelStore } from "./MixPanelStore"; import { ModalStore } from "./ModalStore"; -import OracleStore from "./OracleStore"; -import SpotOrderBookStore from "./SpotOrderBookStore"; -import SwapStore from "./SwapStore"; +import { NotificationStore } from "./NotificationStore"; +import { OracleStore } from "./OracleStore"; +import { PerpCreateOrderStore } from "./PerpCreateOrderStore"; +import { PerpMarketInfoStore } from "./PerpMarketInfoStore"; +import { PerpOrderBookStore } from "./PerpOrderBookStore"; +import { PerpTableStore } from "./PerpTableStore"; +import { QuickAssetsStore } from "./QuickAssetsStore"; +import { ISerializedSettingStore, SettingsStore } from "./SettingsStore"; +import { SpotCreateOrderStore } from "./SpotCreateOrderStore"; +import { SpotMarketInfoStore } from "./SpotMarketInfoStore"; +import { SpotOrderBookStore } from "./SpotOrderBookStore"; +import { SpotTableStore } from "./SpotTableStore"; +import { SwapStore } from "./SwapStore"; export interface ISerializedRootStore { accountStore?: ISerializedAccountStore; - tradeStore?: ISerializedTradeStore; + marketStore?: ISerializedMarketStore; settingStore?: ISerializedSettingStore; } export default class RootStore { static instance?: RootStore; + + // Utility Stores accountStore: AccountStore; oracleStore: OracleStore; - faucetStore: FaucetStore; + marketStore: MarketStore; settingsStore: SettingsStore; notificationStore: NotificationStore; - tradeStore: TradeStore; balanceStore: BalanceStore; modalStore: ModalStore; - swapStore: SwapStore; mixPanelStore: MixPanelStore; quickAssetsStore: QuickAssetsStore; + + swapStore: SwapStore; + faucetStore: FaucetStore; + + // Spot Stores + spotCreateOrderStore: SpotCreateOrderStore; + spotMarketInfoStore: SpotMarketInfoStore; spotOrderBookStore: SpotOrderBookStore; + spotTableStore: SpotTableStore; + + // Prep Stores + perpCreateOrderStore: PerpCreateOrderStore; + perpMarketInfoStore: PerpMarketInfoStore; + perpOrderBookStore: PerpOrderBookStore; + perpTableStore: PerpTableStore; + dashboardStore: DashboardStore; leaderboardStore: LeaderboardStore; private constructor(initState?: ISerializedRootStore) { - this.notificationStore = new NotificationStore(this); this.accountStore = new AccountStore(this, initState?.accountStore); this.oracleStore = new OracleStore(this); - this.faucetStore = new FaucetStore(this); + this.marketStore = new MarketStore(this, initState?.marketStore); this.settingsStore = new SettingsStore(this, initState?.settingStore); - this.tradeStore = new TradeStore(this, initState?.tradeStore); + this.notificationStore = new NotificationStore(this); this.balanceStore = new BalanceStore(this); this.modalStore = new ModalStore(this); - this.swapStore = new SwapStore(this); this.mixPanelStore = new MixPanelStore(this); this.quickAssetsStore = new QuickAssetsStore(this); + + this.faucetStore = new FaucetStore(this); + this.swapStore = new SwapStore(this); + + this.spotCreateOrderStore = new SpotCreateOrderStore(this); + this.spotMarketInfoStore = new SpotMarketInfoStore(this); this.spotOrderBookStore = new SpotOrderBookStore(this); + this.spotTableStore = new SpotTableStore(this); + this.perpTableStore = new PerpTableStore(this); + + this.perpCreateOrderStore = new PerpCreateOrderStore(this); + this.perpMarketInfoStore = new PerpMarketInfoStore(this); + this.perpOrderBookStore = new PerpOrderBookStore(this); + this.dashboardStore = new DashboardStore(this); this.leaderboardStore = new LeaderboardStore(this); @@ -76,12 +108,12 @@ export default class RootStore { }; get initialized() { - return this.accountStore.initialized && this.tradeStore.initialized; + return this.accountStore.initialized && this.marketStore.initialized; } serialize = (): ISerializedRootStore => ({ accountStore: this.accountStore.serialize(), - tradeStore: this.tradeStore.serialize(), settingStore: this.settingsStore.serialize(), + marketStore: this.marketStore.serialize(), }); } diff --git a/src/stores/SettingsStore.ts b/src/stores/SettingsStore.ts index 9672c86b..c676c852 100644 --- a/src/stores/SettingsStore.ts +++ b/src/stores/SettingsStore.ts @@ -4,9 +4,9 @@ import { LimitType } from "@compolabs/spark-orderbook-ts-sdk"; import { THEME_TYPE } from "@themes/ThemeProvider"; -import RootStore from "@stores/RootStore"; +import { ORDER_TYPE } from "@stores/SpotCreateOrderStore"; -import { ORDER_TYPE } from "@screens/SpotScreen/RightBlock/CreateOrder/CreateOrderVM"; +import RootStore from "./RootStore"; export interface ISerializedSettingStore { isUserAgreedWithTerms?: boolean; @@ -25,7 +25,7 @@ export enum TRADE_TABLE_SIZE { AUTO, } -class SettingsStore { +export class SettingsStore { private readonly rootStore: RootStore; selectedTheme: THEME_TYPE = THEME_TYPE.DARK_THEME; @@ -75,5 +75,3 @@ class SettingsStore { orderType: this.orderType, }); } - -export default SettingsStore; diff --git a/src/screens/SpotScreen/RightBlock/CreateOrder/CreateOrderVM.tsx b/src/stores/SpotCreateOrderStore.ts similarity index 75% rename from src/screens/SpotScreen/RightBlock/CreateOrder/CreateOrderVM.tsx rename to src/stores/SpotCreateOrderStore.ts index 2e551659..d69c4341 100644 --- a/src/screens/SpotScreen/RightBlock/CreateOrder/CreateOrderVM.tsx +++ b/src/stores/SpotCreateOrderStore.ts @@ -1,4 +1,3 @@ -import React, { PropsWithChildren, useMemo } from "react"; import _ from "lodash"; import { makeAutoObservable, reaction } from "mobx"; import { Undefinable } from "tsdef"; @@ -13,8 +12,7 @@ import { OrderType, } from "@compolabs/spark-orderbook-ts-sdk"; -import useVM from "@hooks/useVM"; -import { RootStore, useStores } from "@stores"; +import { RootStore } from "@stores"; import { MIXPANEL_EVENTS } from "@stores/MixPanelStore"; import { DEFAULT_DECIMALS } from "@constants"; @@ -28,16 +26,6 @@ import Math from "@utils/Math"; import { FuelNetwork } from "@blockchain"; import { SpotMarket, SpotMarketOrder } from "@entity"; -const ctx = React.createContext(null); - -export const CreateOrderVMProvider: React.FC = ({ children }) => { - const rootStore = useStores(); - const store = useMemo(() => new CreateOrderVM(rootStore), [rootStore]); - return {children}; -}; - -export const useCreateOrderVM = () => useVM(ctx); - const PRICE_UPDATE_THROTTLE_INTERVAL = 1000; // 1s export enum ORDER_MODE { @@ -70,7 +58,7 @@ interface DepositInfo { assetType: AssetType; } -class CreateOrderVM { +export class SpotCreateOrderStore { isLoading = false; mode = ORDER_MODE.BUY; @@ -89,11 +77,11 @@ class CreateOrderVM { constructor(private rootStore: RootStore) { makeAutoObservable(this); - const { tradeStore, spotOrderBookStore, settingsStore } = this.rootStore; + const { spotOrderBookStore, marketStore, settingsStore } = this.rootStore; // TODO: Fix the bug where the price doesn’t change when switching markets reaction( - () => [spotOrderBookStore.buyOrders, spotOrderBookStore.sellOrders], + () => [spotOrderBookStore.allBuyOrders, spotOrderBookStore.allSellOrders], ([buyOrders, sellOrders]) => { const orders = this.isSell ? buyOrders : sellOrders; const order = orders[orders.length - 1]; @@ -105,7 +93,11 @@ class CreateOrderVM { settingsStore.orderType === ORDER_TYPE.Limit && this.inputPrice.isZero() && this.activeInput !== ACTIVE_INPUT.Price; - if (shouldSetMarketPrice || shouldSetDefaultLimitPrice || order.market === tradeStore.market?.contractAddress) { + if ( + shouldSetMarketPrice || + shouldSetDefaultLimitPrice || + order.market === marketStore.market?.contractAddress + ) { this.setInputPriceThrottle(order.price); } }, @@ -114,7 +106,7 @@ class CreateOrderVM { reaction( () => [this.isSell, settingsStore.orderType], ([isSell]) => { - const orders = isSell ? spotOrderBookStore.buyOrders : spotOrderBookStore.sellOrders; + const orders = isSell ? spotOrderBookStore.allBuyOrders : spotOrderBookStore.allSellOrders; const order = orders[orders.length - 1]; if (!order) return; @@ -123,7 +115,7 @@ class CreateOrderVM { ); reaction( - () => tradeStore.market, + () => marketStore.spotMarket, () => { this.setInputAmount(BN.ZERO); this.setInputTotal(BN.ZERO); @@ -139,19 +131,14 @@ class CreateOrderVM { } get isInputError(): boolean { - return this.isSpotInputError; - } + const { balanceStore, marketStore } = this.rootStore; - get isSpotInputError(): boolean { - const { - tradeStore: { market }, - balanceStore, - } = this.rootStore; + if (!marketStore.spotMarket) return false; const amount = this.isSell ? this.inputAmount : this.inputTotal; - const token = this.isSell ? market!.baseToken : market!.quoteToken; + const token = this.isSell ? marketStore.spotMarket.baseToken : marketStore.spotMarket.quoteToken; - const totalBalance = balanceStore.getTotalBalance(token.assetId); + const totalBalance = balanceStore.getSpotTotalBalance(token.assetId); return totalBalance ? amount.gt(totalBalance) : false; } @@ -171,17 +158,13 @@ class CreateOrderVM { }; onMaxClick = () => { - this.onSpotMaxClick(); - }; + const { mixPanelStore, balanceStore, marketStore } = this.rootStore; - private onSpotMaxClick = () => { - const { tradeStore, mixPanelStore, balanceStore } = this.rootStore; + if (!marketStore.spotMarket) return; - if (!tradeStore.market) return; + const token = this.isSell ? marketStore.spotMarket.baseToken : marketStore.spotMarket.quoteToken; - const token = this.isSell ? tradeStore.market.baseToken : tradeStore.market.quoteToken; - - const totalBalance = balanceStore.getTotalBalance(token.assetId); + const totalBalance = balanceStore.getSpotTotalBalance(token.assetId); if (this.isSell) { this.setInputAmount(totalBalance); return; @@ -219,11 +202,11 @@ class CreateOrderVM { }; private calculateInputs(): void { - const { tradeStore } = this.rootStore; - if (!tradeStore.market) return; + const { marketStore, spotMarketInfoStore } = this.rootStore; + if (!marketStore.spotMarket) return; - const baseDecimals = tradeStore.market.baseToken.decimals; - const quoteDecimals = tradeStore.market.quoteToken.decimals; + const baseDecimals = marketStore.spotMarket.baseToken.decimals; + const quoteDecimals = marketStore.spotMarket.quoteToken.decimals; const newInputTotal = Math.multiplyWithDifferentDecimals( this.inputAmount, @@ -253,17 +236,17 @@ class CreateOrderVM { this.updatePercent(); - tradeStore.fetchTradeFeeDebounce(this.inputTotal.toString()); + spotMarketInfoStore.fetchTradeFeeDebounce(this.inputTotal.toString()); } private updatePercent(): void { - const { tradeStore, balanceStore } = this.rootStore; + const { balanceStore, marketStore } = this.rootStore; - if (!tradeStore.market) return; + if (!marketStore.spotMarket) return; - const token = this.isSell ? tradeStore.market.baseToken : tradeStore.market.quoteToken; + const token = this.isSell ? marketStore.spotMarket.baseToken : marketStore.spotMarket.quoteToken; - const totalBalance = balanceStore.getTotalBalance(token.assetId); + const totalBalance = balanceStore.getSpotTotalBalance(token.assetId); if (totalBalance.isZero()) { this.inputPercent = BN.ZERO; @@ -282,35 +265,41 @@ class CreateOrderVM { setInputPercent = (value: number | number[]) => (this.inputPercent = new BN(value.toString())); createOrder = async () => { - const { tradeStore, notificationStore, balanceStore, mixPanelStore, settingsStore, accountStore } = this.rootStore; + const { + marketStore, + notificationStore, + balanceStore, + mixPanelStore, + settingsStore, + accountStore, + spotMarketInfoStore, + } = this.rootStore; - const { market } = tradeStore; - const { timeInForce } = settingsStore; const bcNetwork = FuelNetwork.getInstance(); - if (!market) return; + if (!marketStore.spotMarket) return; + this.isLoading = true; const isBuy = this.mode === ORDER_MODE.BUY; const type = isBuy ? OrderType.Buy : OrderType.Sell; - const fee = getRealFee(market, tradeStore.matcherFee, tradeStore.exchangeFee, !isBuy); + const fee = getRealFee(marketStore.market, spotMarketInfoStore.matcherFee, spotMarketInfoStore.exchangeFee, !isBuy); const depositAmount = isBuy ? this.inputTotal : this.inputAmount; const depositAmountWithFee = fee.exchangeFee.plus(fee.matcherFee); - console.log(depositAmountWithFee.toString()); const deposit: DepositInfo = { amountToSpend: depositAmount.toString(), amountFee: depositAmountWithFee.toString(), - depositAssetId: isBuy ? market.quoteToken.assetId : market.baseToken.assetId, - feeAssetId: market.quoteToken.assetId, + depositAssetId: isBuy ? marketStore.spotMarket.quoteToken.assetId : marketStore.spotMarket.baseToken.assetId, + feeAssetId: marketStore.spotMarket.quoteToken.assetId, assetType: isBuy ? AssetType.Quote : AssetType.Base, }; const marketContractsByType = isBuy - ? CONFIG.MARKETS.filter((m) => m.quoteAssetId.toLowerCase() === deposit.feeAssetId.toLowerCase()) - : CONFIG.MARKETS.filter((m) => m.baseAssetId.toLowerCase() === deposit.depositAssetId.toLowerCase()); + ? CONFIG.SPOT.MARKETS.filter((m) => m.quoteAssetId.toLowerCase() === deposit.feeAssetId.toLowerCase()) + : CONFIG.SPOT.MARKETS.filter((m) => m.baseAssetId.toLowerCase() === deposit.depositAssetId.toLowerCase()); const marketContracts = marketContractsByType.map((m) => m.contractId); @@ -321,19 +310,19 @@ class CreateOrderVM { try { let hash: Undefinable = ""; - if (timeInForce === LimitType.GTC) { + if (settingsStore.timeInForce === LimitType.GTC) { hash = await this.createGTCOrder(type, deposit, marketContracts); } else { - hash = await this.createMarketOrLimitOrder(type, market, deposit, marketContracts); + hash = await this.createMarketOrLimitOrder(type, marketStore.spotMarket, deposit, marketContracts); } - const token = isBuy ? market.baseToken : market.quoteToken; + const token = isBuy ? marketStore.spotMarket.baseToken : marketStore.spotMarket.quoteToken; const amount = isBuy ? this.inputAmount : this.inputTotal; this.setInputTotal(BN.ZERO); mixPanelStore.trackEvent(MIXPANEL_EVENTS.CONFIRM_ORDER, { order_type: isBuy ? "BUY" : "SELL", - token_1: market.baseToken.symbol, - token_2: market.quoteToken.symbol, + token_1: marketStore.spotMarket.baseToken.symbol, + token_2: marketStore.spotMarket.quoteToken.symbol, transaction_sum: BN.formatUnits(amount, token.decimals).toSignificant(2), user_address: accountStore.address, }); @@ -373,7 +362,7 @@ class CreateOrderVM { ...deposit, }; - const data = await bcNetwork.createSpotOrderWithDeposit(order, marketContracts); + const data = await bcNetwork.spotCreateOrderWithDeposit(order, marketContracts); return data.transactionId; }; @@ -383,7 +372,7 @@ class CreateOrderVM { deposit: DepositInfo, marketContracts: string[], ): Promise => { - const { settingsStore, tradeStore } = this.rootStore; + const { settingsStore } = this.rootStore; const bcNetwork = FuelNetwork.getInstance(); const isBuy = type === OrderType.Buy; @@ -395,7 +384,7 @@ class CreateOrderVM { orderType: isBuy ? OrderType.Sell : OrderType.Buy, }; - const activeOrders = await bcNetwork.fetchSpotActiveOrders(params); + const activeOrders = await bcNetwork.spotFetchActiveOrders(params); let orders: SpotMarketOrder[] = []; @@ -429,10 +418,8 @@ class CreateOrderVM { ? orderList[orderList.length - 1].price.toString() : this.inputPrice.toString(); - if (!tradeStore.market) return ""; - - const baseDecimals = tradeStore.market.baseToken.decimals; - const quoteDecimals = tradeStore.market.quoteToken.decimals; + const baseDecimals = market.baseToken.decimals; + const quoteDecimals = market.quoteToken.decimals; const newInputTotal = this.inputTotal.minus(deposit.amountFee); const fullPrecent = "10000"; const slippage = @@ -455,7 +442,7 @@ class CreateOrderVM { orders: orderList.map((el) => el.id), slippage: slippage, }; - const data = await bcNetwork.fulfillOrderManyWithDeposit(order, marketContracts); + const data = await bcNetwork.spotFulfillOrderManyWithDeposit(order, marketContracts); return data.transactionId; }; diff --git a/src/stores/SpotMarketInfoStore.ts b/src/stores/SpotMarketInfoStore.ts new file mode 100644 index 00000000..1b7472ce --- /dev/null +++ b/src/stores/SpotMarketInfoStore.ts @@ -0,0 +1,163 @@ +import { toBech32 } from "fuels"; +import _ from "lodash"; +import { makeAutoObservable, reaction } from "mobx"; + +import { DEFAULT_DECIMALS } from "@constants"; +import BN from "@utils/BN"; +import { IntervalUpdater } from "@utils/IntervalUpdater"; + +import { FuelNetwork } from "@blockchain"; +import { SpotMarketVolume } from "@blockchain/types"; + +import RootStore from "./RootStore"; + +const MARKET_INFO_UPDATE_INTERVAL = 5 * 60 * 1000; // 5 min + +export class SpotMarketInfoStore { + private readonly rootStore: RootStore; + + marketInfo: SpotMarketVolume = { + volume: BN.ZERO, + high: BN.ZERO, + low: BN.ZERO, + }; + + matcherFee = BN.ZERO; + tradeFee = { + makerFee: BN.ZERO, + takerFee: BN.ZERO, + }; + + minimalOrder = { + minPrice: BN.ZERO, + minOrder: BN.ZERO, + }; + + isTradeFeeLoading = false; + isMatcherFeeLoading = false; + + private marketInfoUpdater: IntervalUpdater; + + constructor(rootStore: RootStore) { + this.rootStore = rootStore; + makeAutoObservable(this); + + const { oracleStore, marketStore } = rootStore; + + this.marketInfoUpdater = new IntervalUpdater(this.updateMarketInfo, MARKET_INFO_UPDATE_INTERVAL); + + reaction( + () => [marketStore.market, oracleStore.initialized], + () => { + this.updateMarketInfo(); + this.fetchMatcherFee(); + this.getMinimalOrder(); + }, + { fireImmediately: true }, + ); + + this.marketInfoUpdater.run(true); + } + + get isFeeLoading(): boolean { + return this.isTradeFeeLoading || this.isMatcherFeeLoading; + } + + get exchangeFee(): BN { + const { makerFee, takerFee } = this.tradeFee; + + return BN.max(makerFee, takerFee); + } + + get exchangeFeeFormat(): BN { + const { marketStore } = this.rootStore; + if (!marketStore.spotMarket) return BN.ZERO; + + const decimals = marketStore.spotMarket.quoteToken.decimals; + return BN.formatUnits(this.exchangeFee, decimals); + } + + get matcherFeeFormat(): BN { + const { marketStore } = this.rootStore; + if (!marketStore.spotMarket) return BN.ZERO; + + const decimals = marketStore.spotMarket.quoteToken.decimals; + return BN.formatUnits(this.matcherFee, decimals); + } + + getIsEnoughtMoneyForFee(isSell: boolean) { + const { marketStore } = this.rootStore; + + if (!marketStore.spotMarket || isSell) return true; + const { balanceStore } = this.rootStore; + + const walletAmount = balanceStore.getWalletBalance(marketStore.spotMarket.quoteToken.assetId); + + return this.exchangeFee.plus(this.matcherFee).lte(walletAmount); + } + + getMinimalOrder = async () => { + const bcNetwork = FuelNetwork.getInstance(); + const [order, price] = await Promise.all([bcNetwork.spotFetchMinOrderSize(), bcNetwork.spotFetchMinOrderPrice()]); + this.minimalOrder = { + minPrice: new BN(price), + minOrder: new BN(order), + }; + }; + + updateMarketInfo = async () => { + const { marketStore } = this.rootStore; + if (!marketStore.spotMarket) return; + + const info = await FuelNetwork.getInstance().spotFetchVolume({ + limit: 1000, + market: [marketStore.spotMarket.contractAddress], + }); + + const volume = BN.formatUnits(info.volume, marketStore.spotMarket.baseToken.decimals); + const low = BN.formatUnits(info.low, DEFAULT_DECIMALS); + const high = BN.formatUnits(info.high, DEFAULT_DECIMALS); + + this.marketInfo = { + volume, + low, + high, + }; + }; + + fetchMatcherFee = async () => { + const { marketStore } = this.rootStore; + const bcNetwork = FuelNetwork.getInstance(); + + if (!marketStore.spotMarket) return; + + this.isMatcherFeeLoading = true; + const matcherFee = await bcNetwork.spotFetchMatcherFee(); + + this.matcherFee = new BN(matcherFee); + this.isMatcherFeeLoading = false; + }; + + private fetchTradeFee = async (quoteAmount: string) => { + const { accountStore, marketStore } = this.rootStore; + + if (new BN(quoteAmount).isZero()) { + this.tradeFee = { makerFee: BN.ZERO, takerFee: BN.ZERO }; + return; + } + + const bcNetwork = FuelNetwork.getInstance(); + + if (!accountStore.address || !marketStore.spotMarket) return; + + this.isTradeFeeLoading = true; + const address = toBech32(accountStore.address!); + + const { makerFee, takerFee } = await bcNetwork.spotFetchProtocolFeeAmountForUser(quoteAmount, address); + + this.tradeFee = { makerFee: new BN(makerFee), takerFee: new BN(takerFee) }; + this.isTradeFeeLoading = false; + }; + + fetchTradeFeeDebounce = _.debounce(this.fetchTradeFee, 250); +} diff --git a/src/stores/SpotOrderBookStore.ts b/src/stores/SpotOrderBookStore.ts index 6357e257..dab04c38 100644 --- a/src/stores/SpotOrderBookStore.ts +++ b/src/stores/SpotOrderBookStore.ts @@ -6,11 +6,11 @@ import { GetActiveOrdersParams, OrderType } from "@compolabs/spark-orderbook-ts- import { RootStore } from "@stores"; -import { SPOT_ORDER_FILTER } from "@screens/SpotScreen/OrderbookAndTradesInterface/SpotOrderBook/SpotOrderBook"; +import { SPOT_ORDER_FILTER } from "@screens/SpotScreen/OrderbookAndTrades/SpotOrderBook/SpotOrderBook"; import { DEFAULT_DECIMALS } from "@constants"; import BN from "@utils/BN"; -import { formatSpotMarketOrders } from "@utils/formatSpotMarketOrders"; +import { CONFIG, Market } from "@utils/getConfig.ts"; import { getOhlcvData, OhlcvData } from "@utils/getOhlcvData"; import { groupOrders } from "@utils/groupOrders"; @@ -19,7 +19,11 @@ import { SpotMarketOrder, SpotMarketTrade } from "@entity"; import { Subscription } from "@src/typings/utils"; -class SpotOrderBookStore { +type ExchangeRates = { + [pair: string]: string; +}; + +export class SpotOrderBookStore { private readonly rootStore: RootStore; private subscriptionToTradeOrderEvents: Nullable = null; @@ -39,26 +43,49 @@ class SpotOrderBookStore { ohlcvData: OhlcvData[] = []; historgramData: HistogramData[] = []; + marketPrices: ExchangeRates = {}; constructor(rootStore: RootStore) { this.rootStore = rootStore; makeAutoObservable(this); + const { initialized, marketStore } = this.rootStore; + + setTimeout(() => { + CONFIG.SPOT.MARKETS.map(async (el: Market) => { + const buy = await this.fetchOrderBook(el, OrderType.Buy); + const sell = await this.fetchOrderBook(el, OrderType.Sell); + const price = new BN(buy.priceUnits).plus(sell.priceUnits).div(2).toSignificant(2); + this.marketPrices = { ...this.marketPrices, [el.contractId]: price }; + }); + }, 1000); reaction( - () => [rootStore.initialized, rootStore.tradeStore.market], + () => [initialized, marketStore.market], ([initialized]) => { if (!initialized) return; - this.decimalGroup = this.rootStore.tradeStore.market?.baseToken.precision ?? 4; + this.decimalGroup = this.rootStore.marketStore.market?.baseToken.precision ?? 4; + this.updateOrderBook(); + }, + { fireImmediately: true }, + ); + + reaction( + () => [this.rootStore.marketStore.market, this.rootStore.initialized], + ([market, initialized]) => { + if (!initialized || !market) return; + this.updateOrderBook(); + this.decimalGroup = this.rootStore.marketStore.market?.baseToken.precision ?? 4; + this.subscribeTrades(); }, { fireImmediately: true }, ); reaction( - () => [this.rootStore.tradeStore.market, this.rootStore.initialized], + () => [this.rootStore.marketStore.market, this.rootStore.initialized], ([market, initialized]) => { if (!initialized || !market) return; - this.decimalGroup = this.rootStore.tradeStore.market?.baseToken.precision ?? 4; + this.decimalGroup = this.rootStore.marketStore.market?.baseToken.precision ?? 4; this.subscribeTrades(); }, { fireImmediately: true }, @@ -111,8 +138,8 @@ class SpotOrderBookStore { setOrderFilter = (value: SPOT_ORDER_FILTER) => (this.orderFilter = value); updateOrderBook = () => { - const { tradeStore } = this.rootStore; - const market = tradeStore.market; + const { marketStore } = this.rootStore; + const market = marketStore.spotMarket; if (!this.rootStore.initialized || !market) return; @@ -128,6 +155,28 @@ class SpotOrderBookStore { this.subscribeToSellOrders(bcNetwork, params); }; + async fetchOrderBook(market: Market, orderType: OrderType) { + const bcNetwork = FuelNetwork.getInstance(); + const params: GetActiveOrdersParams = { + limit: 1, + market: [market.contractId], + asset: market.baseAssetId ?? "", + orderType: orderType, + }; + const activeOrders = await bcNetwork.spotFetchActiveOrders(params); + if ("ActiveSellOrder" in activeOrders.data) { + return new SpotMarketOrder({ + ...activeOrders.data.ActiveSellOrder[0], + quoteAssetId: market.quoteAssetId, + }); + } else { + return new SpotMarketOrder({ + ...activeOrders.data.ActiveBuyOrder[0], + quoteAssetId: market.quoteAssetId, + }); + } + } + private subscribeToOrders( orderType: OrderType, subscription: Subscription | null, @@ -139,24 +188,26 @@ class SpotOrderBookStore { subscription.unsubscribe(); } - const { tradeStore } = this.rootStore; - const market = tradeStore.market; + const { marketStore } = this.rootStore; + const market = marketStore.spotMarket; - const newSubscription = bcNetwork.subscribeSpotActiveOrders({ ...params, orderType }).subscribe({ + const newSubscription = bcNetwork.spotSubscribeActiveOrders({ ...params, orderType }).subscribe({ next: ({ data }) => { this.isOrderBookLoading = false; if (!data) return; - const orders = formatSpotMarketOrders( - "ActiveBuyOrder" in data ? data.ActiveBuyOrder : data.ActiveSellOrder, - market!.quoteToken.assetId, + const orders = ("ActiveBuyOrder" in data ? data.ActiveBuyOrder : data.ActiveSellOrder).map( + (order) => + new SpotMarketOrder({ + ...order, + quoteAssetId: market!.quoteToken.assetId, + }), ); const orderWithoutBadOrder = orders.filter( (o) => o.id.toLowerCase() !== "0xb140a6bf39601d69d0fedacb61ecce95cb65eaa05856583cb1a9af926acbd5bd".toLowerCase(), ); - updateOrders(orderWithoutBadOrder); }, }); @@ -224,9 +275,18 @@ class SpotOrderBookStore { return BN.ratioOf(this.spread, this.getMaxBuyPrice).toFormat(2); } + get marketPrice(): string { + const { marketStore } = this.rootStore; + return this.marketPrices[marketStore.market?.contractAddress ?? ""]; + } + + marketPriceByContractId(contractAddress: string): string { + return this.marketPrices[contractAddress]; + } + subscribeTrades = () => { - const { tradeStore } = this.rootStore; - const market = tradeStore.market; + const { marketStore } = this.rootStore; + const market = marketStore.spotMarket; const bcNetwork = FuelNetwork.getInstance(); @@ -235,7 +295,7 @@ class SpotOrderBookStore { } this.subscriptionToTradeOrderEvents = bcNetwork - .subscribeSpotTradeOrderEvents({ + .spotSubscribeTradeOrderEvents({ limit: 500, market: [market!.contractAddress], }) @@ -268,5 +328,3 @@ class SpotOrderBookStore { return !this.isInitialLoadComplete; } } - -export default SpotOrderBookStore; diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableVM.tsx b/src/stores/SpotTableStore.ts similarity index 80% rename from src/screens/SpotScreen/BottomTables/SpotTable/SpotTableVM.tsx rename to src/stores/SpotTableStore.ts index dc47e90a..376269d6 100644 --- a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableVM.tsx +++ b/src/stores/SpotTableStore.ts @@ -1,42 +1,36 @@ -import React, { PropsWithChildren, useMemo } from "react"; import { makeAutoObservable, reaction } from "mobx"; import { Nullable } from "tsdef"; import { OrderType, UserInfo } from "@compolabs/spark-orderbook-ts-sdk"; -import useVM from "@hooks/useVM"; -import { RootStore, useStores } from "@stores"; +import { RootStore } from "@stores"; -import { formatSpotMarketOrders } from "@utils/formatSpotMarketOrders"; import { ACTION_MESSAGE_TYPE, getActionMessage } from "@utils/getActionMessage"; import { handleWalletErrors } from "@utils/handleWalletErrors"; import { FuelNetwork } from "@blockchain"; -import { SpotMarketOrder } from "@entity"; +import { SpotMarket, SpotMarketOrder } from "@entity"; import { Subscription } from "@src/typings/utils"; -const ctx = React.createContext(null); - -export const SpotTableVMProvider: React.FC = ({ children }) => { - const rootStore = useStores(); - const store = useMemo(() => new SpotTableVM(rootStore), [rootStore]); - return {children}; -}; - -export const useSpotTableVMProvider = () => useVM(ctx); - const sortDesc = (a: SpotMarketOrder, b: SpotMarketOrder) => b.timestamp.valueOf() - a.timestamp.valueOf(); -class SpotTableVM { +export const PAGINATION_LIMIT = 10; + +export class SpotTableStore { private readonly rootStore: RootStore; private subscriptionToOpenOrders: Nullable = null; private subscriptionToHistoryOrders: Nullable = null; private subscriptionToOrdersStats: Nullable = null; userOrders: SpotMarketOrder[] = []; + private setUserOrders = (orders: SpotMarketOrder[]) => (this.userOrders = orders); + userOrdersHistory: SpotMarketOrder[] = []; + private setUserOrdersHistory = (orders: SpotMarketOrder[]) => (this.userOrdersHistory = orders); + userOrdersStats: Nullable = null; + private setUserOrdersStats = (stats: UserInfo) => (this.userOrdersStats = stats); isOrderCancelling = false; cancelingOrderId: Nullable = null; @@ -46,9 +40,15 @@ class SpotTableVM { isOpenOrdersLoaded = false; isHistoryOrdersLoaded = false; - // filters offset = 0; - limit = 10; + setOffset = (currentPage: number) => { + if (currentPage === 0) { + this.offset = 0; + return; + } + + this.offset = (currentPage - 1) * PAGINATION_LIMIT; + }; filterIsSellOrderTypeEnabled = true; filterIsBuyOrderTypeEnabled = true; @@ -75,11 +75,13 @@ class SpotTableVM { constructor(rootStore: RootStore) { makeAutoObservable(this); this.rootStore = rootStore; - const { accountStore, tradeStore } = this.rootStore; + const { accountStore, marketStore } = this.rootStore; reaction( - () => [tradeStore.market, this.rootStore.initialized, accountStore.isConnected, this.tableFilters] as const, + () => [marketStore.market, this.rootStore.initialized, accountStore.isConnected, this.tableFilters] as const, ([market, initialized, isConnected, _]) => { - if (!initialized || !market || !isConnected) { + const isSpotMarket = market && SpotMarket.isInstance(market); + + if (!isSpotMarket || !initialized || !isConnected) { this.setUserOrders([]); this.setUserOrdersHistory([]); return; @@ -104,7 +106,7 @@ class SpotTableVM { : OrderType.Buy; return { - limit: this.limit, + limit: PAGINATION_LIMIT, offset: this.offset, orderType, }; @@ -115,10 +117,10 @@ class SpotTableVM { }; cancelOrder = async (order: SpotMarketOrder) => { - const { notificationStore } = this.rootStore; + const { notificationStore, marketStore } = this.rootStore; const bcNetwork = FuelNetwork.getInstance(); - if (!this.rootStore.tradeStore.market) return; + if (!marketStore.market) return; this.isOrderCancelling = true; this.cancelingOrderId = order.id; @@ -129,7 +131,7 @@ class SpotTableVM { } try { - const bcNetworkCopy = await bcNetwork.chain(); + const bcNetworkCopy = await bcNetwork.spotChain(); console.log("order", order); const tx = await bcNetworkCopy.writeWithMarket(order.market).cancelOrder(order.id); notificationStore.success({ @@ -151,8 +153,8 @@ class SpotTableVM { this.isWithdrawing = true; this.withdrawingAssetId = assetId; - const amount = balanceStore.getContractBalance(assetId); - await balanceStore.withdrawBalance(assetId, amount.toString()); + const amount = balanceStore.getSpotContractBalance(assetId); + await balanceStore.withdrawSpotBalance(assetId, amount.toString()); this.isWithdrawing = false; this.withdrawingAssetId = null; @@ -167,7 +169,7 @@ class SpotTableVM { } this.subscriptionToOpenOrders = bcNetwork - .subscribeSpotOrders({ + .spotSubscribeOrders({ ...this.tableFilters, user: accountStore.address!, status: ["Active"], @@ -176,7 +178,7 @@ class SpotTableVM { next: ({ data }) => { if (!data) return; - const sortedOrder = formatSpotMarketOrders(data.Order).sort(sortDesc); + const sortedOrder = data.Order.map((order) => new SpotMarketOrder(order)).sort(sortDesc); this.setUserOrders(sortedOrder); if (!this.isOpenOrdersLoaded) { @@ -194,7 +196,7 @@ class SpotTableVM { this.subscriptionToHistoryOrders.unsubscribe(); } this.subscriptionToHistoryOrders = bcNetwork - .subscribeSpotOrders({ + .spotSubscribeOrders({ ...this.tableFilters, user: accountStore.address!, status: ["Closed", "Canceled"], @@ -203,7 +205,7 @@ class SpotTableVM { next: ({ data }) => { if (!data) return; - const sortedOrdersHistory = formatSpotMarketOrders(data.Order).sort(sortDesc); + const sortedOrdersHistory = data.Order.map((order) => new SpotMarketOrder(order)).sort(sortDesc); this.setUserOrdersHistory(sortedOrdersHistory); if (!this.isHistoryOrdersLoaded) { @@ -228,7 +230,7 @@ class SpotTableVM { } this.subscriptionToOrdersStats = bcNetwork - .subscribeUserInfo({ + .spotSubscribeUserInfo({ id: accountStore.address!, }) .subscribe({ @@ -240,19 +242,4 @@ class SpotTableVM { }, }); }; - - private setUserOrders = (orders: SpotMarketOrder[]) => (this.userOrders = orders); - - private setUserOrdersHistory = (orders: SpotMarketOrder[]) => (this.userOrdersHistory = orders); - - private setUserOrdersStats = (stats: UserInfo) => (this.userOrdersStats = stats); - - setOffset = (currentPage: number) => { - if (currentPage === 0) { - this.offset = 0; - return; - } - - this.offset = (currentPage - 1) * this.limit; - }; } diff --git a/src/stores/SwapStore.ts b/src/stores/SwapStore.ts index da160960..670b52c9 100644 --- a/src/stores/SwapStore.ts +++ b/src/stores/SwapStore.ts @@ -1,4 +1,4 @@ -import { autorun, makeAutoObservable, reaction } from "mobx"; +import { makeAutoObservable } from "mobx"; import { AssetType, GetActiveOrdersParams, LimitType, Order, OrderType } from "@compolabs/spark-orderbook-ts-sdk"; @@ -10,11 +10,11 @@ import { handleWalletErrors } from "@utils/handleWalletErrors"; import { parseNumberWithCommas } from "@utils/swapUtils"; import { FuelNetwork } from "@blockchain"; -import { SpotMarketOrder, Token } from "@entity"; +import { SpotMarket, SpotMarketOrder, Token } from "@entity"; import RootStore from "./RootStore"; -class SwapStore { +export class SwapStore { tokens: Token[]; sellToken: Token; buyToken: Token; @@ -36,38 +36,44 @@ class SwapStore { this.buyTokenPrice = this.getPrice(this.buyToken); this.sellTokenPrice = this.getPrice(this.sellToken); - autorun(async () => { - await this.initialize(); - }); - - reaction( - () => [this.payAmount, this.receiveAmount], - () => { - const { tradeStore } = this.rootStore; - - tradeStore.fetchTradeFeeDebounce( - BN.parseUnits( - this.isBuy() ? this.payAmount : this.receiveAmount, - this.isBuy() ? this.sellToken.decimals : this.buyToken.decimals, - ).toString(), - ); - }, - ); + // autorun(async () => { + // await this.initialize(); + // }); + + // reaction( + // () => [this.payAmount, this.receiveAmount], + // () => { + // const { spotMarketInfoStore } = this.rootStore; + // + // spotMarketInfoStore.fetchTradeFeeDebounce( + // BN.parseUnits( + // this.isBuy() ? this.payAmount : this.receiveAmount, + // this.isBuy() ? this.sellToken.decimals : this.buyToken.decimals, + // ).toString(), + // ); + // }, + // ); } async initialize() { - await this.rootStore.balanceStore.initialize(); + // await this.rootStore.balanceStore.initialize(); this.updateTokens(); } isBuy = () => { - const { tradeStore } = this.rootStore; - return tradeStore.market?.baseToken?.assetId === this.buyToken.assetId; + const { marketStore } = this.rootStore; + return marketStore.market?.baseToken?.assetId === this.buyToken.assetId; }; + get spotMarkets() { + const { marketStore } = this.rootStore; + + return marketStore.markets.filter((m) => SpotMarket.isInstance(m)); + } + getTokenPair = (assetId: string) => { - const { tradeStore } = this.rootStore; - const markets = tradeStore.spotMarkets; + const { marketStore } = this.rootStore; + const markets = marketStore.markets.filter((m) => SpotMarket.isInstance(m)); const tokens = this.fetchNewTokens(); return markets .map((market) => { @@ -87,8 +93,7 @@ class SwapStore { } getMarketPair = (baseAsset: Token, quoteToken: Token) => { - const { tradeStore } = this.rootStore; - return tradeStore.spotMarkets.find( + return this.spotMarkets.find( (el) => (el.baseToken.assetId === baseAsset.assetId && el.quoteToken.assetId === quoteToken.assetId) || (el.baseToken.assetId === quoteToken.assetId && el.quoteToken.assetId === baseAsset.assetId), @@ -107,36 +112,25 @@ class SwapStore { fetchNewTokens(): Token[] { const bcNetwork = FuelNetwork.getInstance(); - return bcNetwork!.getTokenList().map((v) => { - const token = bcNetwork!.getTokenByAssetId(v.assetId); - return { - name: token.name, - symbol: token.symbol, - logo: token.logo, - priceFeed: token.priceFeed, - assetId: token.assetId, - decimals: token.decimals, - precision: token.precision, - }; - }); + return bcNetwork!.getTokenList(); } swapTokens = async ({ slippage }: { slippage: number }): Promise => { - const { notificationStore, tradeStore } = this.rootStore; + const { notificationStore, marketStore } = this.rootStore; - if (!tradeStore.market) return false; + if (!marketStore.market) return false; - const { baseToken, quoteToken } = tradeStore.market; + const { baseToken, quoteToken } = marketStore.market; const isBuy = baseToken?.assetId === this.buyToken.assetId; const bcNetwork = FuelNetwork.getInstance(); const params: GetActiveOrdersParams = { limit: 50, // or more if needed - market: [tradeStore.market.contractAddress], + market: [marketStore.market.contractAddress], asset: baseToken?.assetId, orderType: !isBuy ? OrderType.Buy : OrderType.Sell, }; - const activeOrders = await bcNetwork!.fetchSpotActiveOrders(params); + const activeOrders = await bcNetwork!.spotFetchActiveOrders(params); let orders: SpotMarketOrder[] = []; @@ -155,7 +149,7 @@ class SwapStore { // TODO: check if there is enough price sum to fulfill the order const formattedAmount = BN.parseUnits(this.payAmount, this.sellToken.decimals).toString(); const formattedVolume = BN.parseUnits(this.receiveAmount, this.buyToken.decimals).toString(); - // const depositAmountWithFee = tradeStore.exchangeFee.plus(this.rootStore.tradeStore.matcherFee); + // const depositAmountWithFee = spotMarketInfoStore.exchangeFee.plus(spotMarketInfoStore.matcherFee); const depositAmountWithFee = BN.ZERO.plus(BN.ZERO); // TODO: Fix it const pair = this.getMarketPair(this.buyToken, this.sellToken); @@ -198,8 +192,8 @@ class SwapStore { ).toSignificant(2); try { - const marketContracts = CONFIG.MARKETS.map((el) => el.contractId); - const tx = await bcNetwork.fulfillOrderManyWithDeposit(order, marketContracts); + const marketContracts = CONFIG.SPOT.MARKETS.map((el) => el.contractId); + const tx = await bcNetwork.spotFulfillOrderManyWithDeposit(order, marketContracts); notificationStore.success({ text: getActionMessage(ACTION_MESSAGE_TYPE.CREATING_SWAP)( amountFormatted, @@ -258,5 +252,3 @@ class SwapStore { this.receiveAmount = value; } } - -export default SwapStore; diff --git a/src/stores/TradeStore.ts b/src/stores/TradeStore.ts deleted file mode 100644 index 5b10c817..00000000 --- a/src/stores/TradeStore.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { toBech32 } from "fuels"; -import _ from "lodash"; -import { makeAutoObservable, reaction } from "mobx"; -import { Nullable } from "tsdef"; - -import RootStore from "@stores/RootStore"; - -import { DEFAULT_DECIMALS, DEFAULT_MARKET } from "@constants"; -import BN from "@utils/BN"; -import { CONFIG } from "@utils/getConfig"; -import { IntervalUpdater } from "@utils/IntervalUpdater"; - -import { FuelNetwork } from "@blockchain"; -import { SpotMarketVolume } from "@blockchain/types"; -import { PerpMarket, SpotMarket } from "@entity"; - -export interface ISerializedTradeStore { - favMarkets: Nullable; -} - -const MARKET_INFO_UPDATE_INTERVAL = 5 * 60 * 1000; // 5 min -const MARKET_PRICES_UPDATE_INTERVAL = 5 * 1000; // 5 sec - -class TradeStore { - private readonly rootStore: RootStore; - - favMarkets: string[] = []; - spotMarkets: SpotMarket[] = []; - marketSelectionOpened = false; - marketSymbol = DEFAULT_MARKET; - - spotMarketInfo: SpotMarketVolume = { - volume: BN.ZERO, - high: BN.ZERO, - low: BN.ZERO, - }; - - matcherFee = BN.ZERO; - tradeFee = { - makerFee: BN.ZERO, - takerFee: BN.ZERO, - }; - - minimalOrder = { - minPrice: BN.ZERO, - minOrder: BN.ZERO, - }; - - isTradeFeeLoading = false; - isMatcherFeeLoading = false; - - private marketInfoUpdater: IntervalUpdater; - private marketPricesUpdater: IntervalUpdater; - - constructor(rootStore: RootStore, initState?: ISerializedTradeStore) { - this.rootStore = rootStore; - makeAutoObservable(this); - - const { oracleStore } = rootStore; - - if (initState) { - const favMarkets = initState.favMarkets?.split(",").filter(Boolean); - favMarkets && this.setFavMarkets(favMarkets); - } - - this.initMarket(); - - this.marketInfoUpdater = new IntervalUpdater(this.updateMarketInfo, MARKET_INFO_UPDATE_INTERVAL); - this.marketPricesUpdater = new IntervalUpdater(this.updateMarketPrices, MARKET_PRICES_UPDATE_INTERVAL); - - reaction( - () => [this.market, oracleStore.initialized], - () => { - this.updateMarketInfo(); - this.updateMarketPrices(); - this.fetchMatcherFee(); - this.getMinimalOrder(); - }, - { fireImmediately: true }, - ); - - this.marketInfoUpdater.run(true); - this.marketPricesUpdater.run(); - } - - get market() { - return this.spotMarkets.find((market) => market.symbol === this.marketSymbol); - } - - get initialized() { - // const isMarketInfoReady = !( - // this.spotMarketInfo.high.isZero() || - // this.spotMarketInfo.low.isZero() || - // this.spotMarketInfo.volume.isZero() - // ); - // return Boolean(this.spotMarkets.length && isMarketInfoReady); - return true; - } - - get isFeeLoading(): boolean { - return this.isTradeFeeLoading || this.isMatcherFeeLoading; - } - - get exchangeFee(): BN { - const { tradeStore } = this.rootStore; - const { makerFee, takerFee } = tradeStore.tradeFee; - - return BN.max(makerFee, takerFee); - } - - get exchangeFeeFormat(): BN { - if (!this.market) return BN.ZERO; - - const decimals = this.market.quoteToken.decimals; - return BN.formatUnits(this.exchangeFee, decimals); - } - - get matcherFeeFormat(): BN { - if (!this.market) return BN.ZERO; - - const decimals = this.market.quoteToken.decimals; - return BN.formatUnits(this.matcherFee, decimals); - } - - getIsEnoughtMoneyForFee(isSell: boolean) { - if (!this.market || isSell) return true; - const { balanceStore } = this.rootStore; - - const { quoteToken } = this.market; - const walletAmount = balanceStore.getWalletBalance(quoteToken.assetId); - - return this.exchangeFee.plus(this.matcherFee).lte(walletAmount); - } - - getMinimalOrder = async () => { - const bcNetwork = FuelNetwork.getInstance(); - const [order, price] = await Promise.all([bcNetwork.fetchMinOrderSize(), bcNetwork.fetchMinOrderPrice()]); - this.minimalOrder = { - minPrice: new BN(price), - minOrder: new BN(order), - }; - }; - - setMarketSymbol = (v: string) => (this.marketSymbol = v); - - selectActiveMarket = (marketId?: string) => { - const bcNetwork = FuelNetwork.getInstance(); - - if (!marketId || marketId === this.marketSymbol) return; - - const getMarket = (markets: T[]) => - markets.find((market) => market.symbol === marketId); - - const spotMarket = getMarket(this.spotMarkets); - - if (!spotMarket) return; - - const indexerInfo = CONFIG.APP.indexers[spotMarket.contractAddress as keyof typeof CONFIG.APP.indexers]; - bcNetwork.setActiveMarket(spotMarket.contractAddress, indexerInfo); - - this.setMarketSymbol(marketId!); - }; - - addToFav = (marketId: string) => { - if (!this.favMarkets.includes(marketId)) { - this.setFavMarkets([...this.favMarkets, marketId]); - } - }; - - removeFromFav = (marketId: string) => { - const index = this.favMarkets.indexOf(marketId); - index !== -1 && this.favMarkets.splice(index, 1); - }; - - setMarketSelectionOpened = (s: boolean) => (this.marketSelectionOpened = s); - - updateMarketInfo = async () => { - if (!this.market) return; - - const info = await FuelNetwork.getInstance().fetchSpotVolume({ - limit: 1000, - market: [this.market.contractAddress], - }); - - const volume = BN.formatUnits(info.volume, this.market.baseToken.decimals); - const low = BN.formatUnits(info.low, DEFAULT_DECIMALS); - const high = BN.formatUnits(info.high, DEFAULT_DECIMALS); - - this.spotMarketInfo = { - volume, - low, - high, - }; - }; - - updateMarketPrices = async () => { - const { oracleStore } = this.rootStore; - - this.spotMarkets.forEach((market) => { - const indexPriceBn = market.baseToken.priceFeed - ? new BN(oracleStore.getTokenIndexPrice(market.baseToken.priceFeed)) - : BN.ZERO; - - market.setPrice(indexPriceBn); - }); - }; - - fetchMatcherFee = async () => { - const bcNetwork = FuelNetwork.getInstance(); - - if (!this.market) return; - - this.isMatcherFeeLoading = true; - const matcherFee = await bcNetwork.fetchSpotMatcherFee(); - - this.matcherFee = new BN(matcherFee); - this.isMatcherFeeLoading = false; - }; - - private fetchTradeFee = async (quoteAmount: string) => { - if (new BN(quoteAmount).isZero()) { - this.tradeFee = { makerFee: BN.ZERO, takerFee: BN.ZERO }; - return; - } - - const { accountStore } = this.rootStore; - const bcNetwork = FuelNetwork.getInstance(); - - if (!accountStore.address || !this.market) return; - - this.isTradeFeeLoading = true; - const address = toBech32(accountStore.address!); - - const { makerFee, takerFee } = await bcNetwork.fetchSpotProtocolFeeAmountForUser(quoteAmount, address); - - this.tradeFee = { makerFee: new BN(makerFee), takerFee: new BN(takerFee) }; - this.isTradeFeeLoading = false; - }; - - fetchTradeFeeDebounce = _.debounce(this.fetchTradeFee, 250); - - serialize = (): ISerializedTradeStore => ({ - favMarkets: this.favMarkets.join(","), - }); - - private initMarket = async () => { - await this.initSpotMarket().catch(console.error); - await this.getMinimalOrder(); - }; - - private initSpotMarket = async () => { - const bcNetwork = FuelNetwork.getInstance(); - - try { - const markets = CONFIG.MARKETS.map( - (market) => new SpotMarket(market.baseAssetId, market.quoteAssetId, market.contractId), - ); - - const market = markets[0]; - const indexerInfo = CONFIG.APP.indexers[market.contractAddress as keyof typeof CONFIG.APP.indexers]; - bcNetwork.setActiveMarket(market.contractAddress, indexerInfo); - this.setMarketSymbol(market.symbol); - - this.setSpotMarkets(markets); - await this.updateMarketPrices(); - } catch (error) { - console.error("Error init spot market", error); - } - }; - - private setFavMarkets = (v: string[]) => (this.favMarkets = v); - - private setSpotMarkets = (v: SpotMarket[]) => (this.spotMarkets = v); -} - -export default TradeStore; diff --git a/src/stores/index.ts b/src/stores/index.ts index 8629b8a3..8268ba3a 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,33 +1,25 @@ -import { storesContext, useStores } from "@stores/useStores"; - -import AccountStore from "./AccountStore"; -import DashboardStore from "./DashboardStore"; -import FaucetStore from "./FaucetStore"; -import LeaderboardStore from "./LeaderboardStore.ts"; -import MixPanelStore from "./MixPanelStore"; -import NotificationStore from "./NotificationStore"; -// import SpotOrdersStore from "./SpotOrdersStore"; -import OracleStore from "./OracleStore"; -import QuickAssetsStore from "./QuickAssetsStore"; import RootStore from "./RootStore"; -import SettingsStore from "./SettingsStore"; -import SpotOrderBookStore from "./SpotOrderBookStore"; -import TradeStore from "./TradeStore"; +export { RootStore }; -export { - AccountStore, - DashboardStore, - FaucetStore, - LeaderboardStore, - MixPanelStore, - NotificationStore, - OracleStore, - QuickAssetsStore, - RootStore, - SettingsStore, - SpotOrderBookStore, - storesContext, - // SpotOrdersStore, - TradeStore, - useStores, -}; +export { AccountStore } from "./AccountStore"; +export { BalanceStore } from "./BalanceStore"; +export { DashboardStore } from "./DashboardStore"; +export { FaucetStore } from "./FaucetStore"; +export { LeaderboardStore } from "./LeaderboardStore"; +export { MarketStore } from "./MarketStore"; +export { MixPanelStore } from "./MixPanelStore"; +export { ModalStore } from "./ModalStore"; +export { NotificationStore } from "./NotificationStore"; +export { OracleStore } from "./OracleStore"; +export { PerpCreateOrderStore } from "./PerpCreateOrderStore"; +export { PerpMarketInfoStore } from "./PerpMarketInfoStore"; +export { PerpOrderBookStore } from "./PerpOrderBookStore"; +export { PerpTableStore } from "./PerpTableStore"; +export { QuickAssetsStore } from "./QuickAssetsStore"; +export { SettingsStore } from "./SettingsStore"; +export { SpotCreateOrderStore } from "./SpotCreateOrderStore"; +export { SpotMarketInfoStore } from "./SpotMarketInfoStore"; +export { SpotOrderBookStore } from "./SpotOrderBookStore"; +export { SpotTableStore } from "./SpotTableStore"; +export { SwapStore } from "./SwapStore"; +export { storesContext, useStores } from "./useStores"; diff --git a/src/stores/useStores.ts b/src/stores/useStores.ts index d417f4e3..8e21f405 100644 --- a/src/stores/useStores.ts +++ b/src/stores/useStores.ts @@ -1,6 +1,6 @@ import React from "react"; -import RootStore from "@stores/RootStore"; +import RootStore from "./RootStore"; export const storesContext = React.createContext(null); diff --git a/src/typings/vite-env.d.ts b/src/typings/vite-env.d.ts index d00e6238..46f2059c 100644 --- a/src/typings/vite-env.d.ts +++ b/src/typings/vite-env.d.ts @@ -2,4 +2,13 @@ /// /// /// -/// \ No newline at end of file +/// + +interface ImportMetaEnv { + VITE_FEATURE_TOGGLE_CLIENT_KEY: string | undefined; + VITE_BRANCH_NAME: string | undefined; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/src/utils/formatSpotMarketOrders.ts b/src/utils/formatSpotMarketOrders.ts deleted file mode 100644 index 833decef..00000000 --- a/src/utils/formatSpotMarketOrders.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Order } from "@compolabs/spark-orderbook-ts-sdk"; - -import { SpotMarketOrder } from "@entity"; - -export const formatSpotMarketOrders = (orders: Order[], quoteAssetId?: string) => { - return orders.map( - (order) => - new SpotMarketOrder({ - ...order, - quoteAssetId, - }), - ); -}; diff --git a/src/utils/getConfig.ts b/src/utils/getConfig.ts index 6f22fc28..4e6f6ac4 100644 --- a/src/utils/getConfig.ts +++ b/src/utils/getConfig.ts @@ -20,14 +20,13 @@ export interface Market { } function createConfig() { - const CURRENT_CONFIG_VER = import.meta.env.DEV ? "1.7.0" : "1.7.5"; + const CURRENT_CONFIG_VER = import.meta.env.DEV ? "2.0.0" : "2.0.0"; const configJSON = import.meta.env.DEV ? configDevJSON : configProdJSON; assert(configJSON.version === CURRENT_CONFIG_VER, "Version mismatch"); console.warn("V12 CONFIG", configJSON); - console.log("Contract Ver.", configJSON.contractVer); - const tokens = configJSON.tokens.map(({ name, symbol, decimals, assetId, priceFeed, precision }) => { + const tokens = configJSON.tokens.map(({ name, symbol, decimals, assetId, priceFeed, precision, collateral }) => { return new Token({ name, symbol, @@ -36,12 +35,10 @@ function createConfig() { logo: TOKEN_LOGOS[symbol], priceFeed, precision, + collateral, }); }); - const markets = configJSON.markets as Market[]; - - // TODO: Refactor this workaround that adds duplicate tokens without the 't' prefix. const tokensBySymbol = tokens.reduce( (acc, t) => { acc[t.symbol] = t; @@ -57,7 +54,16 @@ function createConfig() { return { APP: configJSON, - MARKETS: markets, + SPOT: { + MARKETS: configJSON.spot.markets as Market[], + CONTRACTS: configDevJSON.spot.contracts, + INDEXERS: configDevJSON.spot.indexers, + }, + PERP: { + MARKETS: configJSON.perp.markets as Market[], + CONTRACTS: configDevJSON.perp.contracts, + INDEXERS: configDevJSON.perp.indexers, + }, TOKENS: tokens, TOKENS_BY_SYMBOL: tokensBySymbol, TOKENS_BY_ASSET_ID: tokensByAssetId, diff --git a/src/utils/getExplorerLink.ts b/src/utils/getExplorerLink.ts index a2386edb..7598ca78 100644 --- a/src/utils/getExplorerLink.ts +++ b/src/utils/getExplorerLink.ts @@ -1,9 +1,9 @@ import { CONFIG } from "./getConfig"; export const getExplorerLinkByHash = (hash: string) => { - return `${CONFIG.APP.explorerUrl}/tx/${hash}`; + return `${CONFIG.APP.links.explorerUrl}/tx/${hash}`; }; export const getExplorerLinkByAddress = (address: string) => { - return `${CONFIG.APP.explorerUrl}/account/${address}`; + return `${CONFIG.APP.links.explorerUrl}/account/${address}`; }; diff --git a/src/utils/getRealFee.ts b/src/utils/getRealFee.ts index df372a04..5c41b49a 100644 --- a/src/utils/getRealFee.ts +++ b/src/utils/getRealFee.ts @@ -1,9 +1,9 @@ -import { SpotMarket } from "@entity"; +import { BaseMarket } from "@entity"; import BN from "./BN"; //TODO: Delete this file and make refactor in perp branch -export const getRealFee = (market: SpotMarket | undefined, matcherFee: BN, exchangeFee: BN, isSell: boolean) => { +export const getRealFee = (market: BaseMarket | undefined, matcherFee: BN, exchangeFee: BN, isSell: boolean) => { if (!market || isSell) { return { matcherFee: BN.ZERO, diff --git a/src/utils/groupOrders.ts b/src/utils/groupOrders.ts index 9184fbb6..de13684f 100644 --- a/src/utils/groupOrders.ts +++ b/src/utils/groupOrders.ts @@ -1,22 +1,25 @@ import { DEFAULT_DECIMALS } from "@constants"; -import { SpotMarketOrder } from "@entity"; +import { PerpMarketOrder, SpotMarketOrder } from "@entity"; import BN from "./BN"; +type MarketOrder = SpotMarketOrder | PerpMarketOrder; + const roundPrice = (price: BN, decimals: number): BN => { const factor = new BN(10).pow(decimals); return new BN(price.dividedBy(factor).integerValue(BN.ROUND_HALF_UP).multipliedBy(factor)); }; -export const groupOrders = (orders: SpotMarketOrder[], decimals: number): SpotMarketOrder[] => { - const groupedOrders: { [key: string]: SpotMarketOrder } = {}; +export const groupOrders = (orders: T[], decimals: number): T[] => { + const groupedOrders: { [key: string]: T } = {}; + orders.forEach((order) => { const roundedPrice = roundPrice(order.price, DEFAULT_DECIMALS - decimals); const price = roundedPrice.toString(); if (!groupedOrders[price]) { - groupedOrders[price] = new SpotMarketOrder({ + groupedOrders[price] = new (order.constructor as { new (...args: any[]): T })({ id: order.id, status: "Active", user: "", diff --git a/vite.config.ts b/vite.config.ts index c410bad4..145e5812 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -39,7 +39,8 @@ export default defineConfig({ ], // It should help resolve issues with linking the SDK // - // optimizeDeps: { - // exclude: ["@compolabs/spark-orderbook-ts-sdk"], - // }, + optimizeDeps: { + // exclude: ["@compolabs/spark-orderbook-ts-sdk"], + // exclude: ["@compolabs/spark-perpetual-ts-sdk"], + }, });