diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..febbb5c9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/node_modules +/build diff --git a/.env b/.env index a90a95a7..3dfe37f4 100644 --- a/.env +++ b/.env @@ -1,6 +1,10 @@ HTTPS=true # REACT_APP_NODE_URL='ws://localhost:9988' -REACT_APP_NODE_URL='wss://basilisk-rpc.hydration.cloud/' +# testnet +# REACT_APP_NODE_URL='wss://basilisk-testnet-rpc.bsx.fi/' +# rococo +REACT_APP_NODE_URL='wss://rpc-01.basilisk-rococo.hydradx.io/' +# REACT_APP_NODE_URL='wss://amsterdot.eu.ngrok.io' REACT_APP_PROCESSOR_URL='https://bsx-api-testnet.hydration.cloud/graphql' REACT_APP_APP_NAME='Basilisk UI' NODE_OPTIONS=--openssl-legacy-provider diff --git a/.github/workflows/app-e2e-testing-flow.yml b/.github/workflows/app-e2e-testing-flow.yml new file mode 100644 index 00000000..ae89a3a5 --- /dev/null +++ b/.github/workflows/app-e2e-testing-flow.yml @@ -0,0 +1,206 @@ +name: Application E2E Testing Flow +on: + pull_request: + branches: + - develop + push: + branches: + - 'feat/lbp-v1' + +jobs: + build_app: + name: Build UI application + runs-on: macos-11 + steps: + - uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v1 + with: + node-version: 17.3 + + - name: Cache Node Modules for ui-app + id: cache-node-modules-ui-app + uses: actions/cache@v2 + with: + path: node_modules + key: node-modules-ui-app-${{ hashFiles('yarn.lock') }} + + - name: Install Dependencies for ui-app + if: steps.cache-node-modules-ui-app.outputs.cache-hit != 'true' + run: rm -rf node_modules && yarn install --frozen-lockfile + + - name: Update browserslist + run: npx browserslist@latest --update-db + + - name: Build App + run: yarn run build:deployment + env: + CI: false + REACT_APP_GIT_BRANCH: ${{ steps.extract_branch.outputs.branch }} + NODE_OPTIONS: --openssl-legacy-provider + + - name: Upload script files + uses: actions/upload-artifact@v2 + with: + name: script-files + path: ./scripts + + - name: Upload production-ready App build files + uses: actions/upload-artifact@v2 + with: + name: app-build-files + path: ./build + + run_tests: + name: Run tests + runs-on: ubuntu-latest + needs: [build_app] + steps: + - uses: actions/setup-node@v2 + with: + node-version: 17.3 + + - name: Install Node.js HTTP-Server + run: yarn global add http-server + + - uses: actions/checkout@v2 + with: + path: 'ui-app' + + - name: Download artifact - UI app build + uses: actions/download-artifact@v2 + with: + name: app-build-files + path: ./ui-app/build + +# - name: Download artifact - Storybook build +# uses: actions/download-artifact@v2 +# with: +# name: sb-build-files +# path: ./ui-app/storybook-static + + # Prepare Basilisk-api ("develop" branch must be cloned) + - name: Clone Basilisk-api + run: git clone -b feature/dockerize-testnet https://github.com/galacticcouncil/Basilisk-api.git + + - name: Cache Node Modules for Basilisk-api + id: cache-node-modules-basilisk-api + uses: actions/cache@v2 + with: + path: Basilisk-api/node_modules + key: node-modules-basilisk-api-${{ hashFiles('Basilisk-api/yarn.lock') }} + + - name: Install Dependencies for Basilisk-api + if: steps.cache-node-modules-basilisk-api.outputs.cache-hit != 'true' + run: | + cd Basilisk-api + yarn install --frozen-lockfile + # Install NPM deps for running tests + - name: Cache Node Modules for ui-app + id: cache-node-modules-ui-app + uses: actions/cache@v2 + with: + path: ui-app/node_modules + key: node-modules-ui-app-${{ hashFiles('ui-app/yarn.lock') }} + + - name: Install Dependencies for ui-app + if: steps.cache-node-modules-ui-app.outputs.cache-hit != 'true' + run: | + cd ui-app + yarn install --frozen-lockfile + + # Update folders structure + - name: Change folders permissions + run: | + chmod -R 777 Basilisk-api + chmod -R 777 ui-app + + # Run testnet + - name: Run sandbox testnet + shell: bash + timeout-minutes: 10 + run: | + cd Basilisk-api + yarn fullruntime:clean-setup-start + # Double check of testnet status + - name: Wait for Basilisk Node port :9988 + shell: bash + timeout-minutes: 2 + run: . ./ui-app/scripts/gh-actions-wait-for-port.sh 9988 + + # Run UI App + - name: Run UI application + shell: bash + run: | + cd ui-app/build + http-server -s -p 3030 -a 127.0.0.1 & + # Check of UI app status + - name: Wait for UI app port :3030 + shell: bash + timeout-minutes: 2 + run: . ./ui-app/scripts/gh-actions-wait-for-port.sh 3030 + + # Prepare Playwright env + - name: Install OS dependencies for Playwright + run: npx playwright install-deps + + - name: Make e2e testing env vars file visible (required for falnyr/replace-env-vars-action@master) + run: mv ui-app/.env.test.e2e.ci ui-app/e2e-tests-vars.txt + shell: bash + + - name: Prepate E2E Tests Env Variables + uses: falnyr/replace-env-vars-action@master + env: + E2E_TEST_ACCOUNT_NAME_ALICE: ${{ secrets.E2E_TEST_ACCOUNT_NAME_ALICE }} + E2E_TEST_ACCOUNT_PASSWORD_ALICE: ${{ secrets.E2E_TEST_ACCOUNT_PASSWORD_ALICE }} + E2E_TEST_ACCOUNT_SEED_ALICE: ${{ secrets.E2E_TEST_ACCOUNT_SEED_ALICE }} + with: + filename: ui-app/e2e-tests-vars.txt + + - name: Make e2e testing env vars file hidden + run: mv ui-app/e2e-tests-vars.txt ui-app/.env.test.e2e.ci + shell: bash + + # For debug and monitoring purposes + - name: Check Docker containers and ports + if: always() + run: | + docker ps + docker network ls + sudo lsof -i -P -n | grep LISTEN + shell: bash + + # Run e2e tests + - name: Run e2e tests + shell: bash + run: | + cd ui-app + DEBUG=pw:browser* HEADFUL=true xvfb-run --auto-servernum -- yarn test:e2e-ci + env: + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} + + - name: Sleep for 30 seconds (for compiling html reports) + run: sleep 30s + shell: bash + + - name: Upload trace files + if: always() + uses: actions/upload-artifact@v2 + with: + name: traces_screenshots + path: ./ui-app/traces + + - name: Upload e2e tests report file + if: always() + uses: actions/upload-artifact@v2 + with: + name: e2e_tests_report_html + path: ./ui-app/ui-app-e2e-results.html + + - name: Upload testnet logs + if: always() + uses: actions/upload-artifact@v2 + with: + name: testnet-sandbox-logs + path: ./Basilisk-api/testnet-sandbox-logs diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..c8acc1eb --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,45 @@ +name: docker + +on: + push: + branches: + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + galacticcouncil/basilisk-ui + tags: | + type=ref,event=branch + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v3 + with: + push: true + context: . + build-args: | + GITHUB_SHA=${{ github.sha }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.vscode/snipsnap.code-snippets b/.vscode/snipsnap.code-snippets new file mode 100644 index 00000000..53ada59e --- /dev/null +++ b/.vscode/snipsnap.code-snippets @@ -0,0 +1,7 @@ + +404 Not Found + +

404 Not Found

+
nginx
+ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..297bb797 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:latest AS builder + +COPY . . + +ARG GITHUB_SHA +ENV PUBLIC_URL / +ENV REACT_APP_NODE_URL "wss://rpc-01.basilisk-rococo.hydradx.io" +RUN yarn && yarn run build + +FROM node:lts-slim + +RUN npm -g install serve +WORKDIR /app + +COPY --from=builder /build . + +CMD serve -s + diff --git a/README.md b/README.md index 3a3e4154..0c236521 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ yarn install Start Storybook component development environment. ``` -yarn storybook +yarn storybook:start ``` Storybook can be opened at [:6006](http://localhost:6006) @@ -438,4 +438,4 @@ You have to use legacy openssl provider in node 17+. Set this to node options ```shell export NODE_OPTIONS=--openssl-legacy-provider -``` \ No newline at end of file +``` diff --git a/graphql.schema.json b/graphql.schema.json index 0bcf504f..eab66773 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -3,7 +3,9 @@ "queryType": { "name": "Query" }, - "mutationType": null, + "mutationType": { + "name": "Mutation" + }, "subscriptionType": null, "types": [ { @@ -14,7 +16,24 @@ { "name": "balances", "description": null, - "args": [], + "args": [ + { + "name": "assetIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], "type": { "kind": "NON_NULL", "name": null, @@ -86,10 +105,37 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "vesting", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Vesting", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, - "interfaces": [], + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Balances", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "IVesting", + "ofType": null + } + ], "enumValues": null, "possibleTypes": null }, @@ -214,6 +260,69 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INTERFACE", + "name": "Balances", + "description": null, + "fields": [ + { + "name": "balances", + "description": null, + "args": [ + { + "name": "assetIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Balance", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Query", + "ofType": null + } + ] + }, { "kind": "SCALAR", "name": "Boolean", @@ -224,6 +333,29 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "ChromeExtension", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "POLKADOTJS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TALISMAN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Config", @@ -305,8 +437,8 @@ "description": null, "args": [], "type": { - "kind": "OBJECT", - "name": "Extension", + "kind": "ENUM", + "name": "ChromeExtension", "ofType": null }, "isDeprecated": false, @@ -428,6 +560,40 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INTERFACE", + "name": "IVesting", + "description": null, + "fields": [ + { + "name": "vesting", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Vesting", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Query", + "ofType": null + } + ] + }, { "kind": "OBJECT", "name": "LBPAssetWeights", @@ -784,6 +950,41 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Mutation", + "description": null, + "fields": [ + { + "name": "_empty", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "setActiveAccount", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Account", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "UNION", "name": "Pool", @@ -846,6 +1047,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "_vestingSchedule", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "VestingSchedule", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "accounts", "description": null, @@ -875,13 +1088,9 @@ "description": null, "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Account", - "ofType": null - } + "kind": "OBJECT", + "name": "Account", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -909,7 +1118,24 @@ { "name": "balances", "description": null, - "args": [], + "args": [ + { + "name": "assetIds", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], "type": { "kind": "NON_NULL", "name": null, @@ -1052,24 +1278,51 @@ "description": null, "args": [], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "NON_NULL", + "kind": "LIST", "name": null, "ofType": { - "kind": "UNION", - "name": "Pool", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "UNION", + "name": "Pool", + "ofType": null + } } } }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "vesting", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Vesting", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, - "interfaces": [], + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Balances", + "ofType": null + }, + { + "kind": "INTERFACE", + "name": "IVesting", + "ofType": null + } + ], "enumValues": null, "possibleTypes": null }, @@ -1106,6 +1359,140 @@ ], "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "Vesting", + "description": null, + "fields": [ + { + "name": "claimableAmount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lockedVestingBalance", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "originalLockBalance", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VestingSchedule", + "description": null, + "fields": [ + { + "name": "perPeriod", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "period", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "periodCount", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "start", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "XYKPool", @@ -1178,6 +1565,38 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "shareTokenId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalLiquidity", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -2094,15 +2513,6 @@ } ], "directives": [ - { - "name": "client", - "description": null, - "isRepeatable": false, - "locations": [ - "FIELD" - ], - "args": [] - }, { "name": "deprecated", "description": "Marks an element of a GraphQL schema as no longer supported.", diff --git a/package.json b/package.json index ed1e9722..4c9f7ba8 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "graphql": "^16.3.0", "graphql.macro": "^1.4.2", "husky": "^7.0.4", - "hydra-dx-wasm": "https://github.com/galacticcouncil/HydraDX-wasm#main", + "hydra-dx-wasm": "https://github.com/galacticcouncil/HydraDX-wasm#0e3d625c22c32525a4619047223cac019c0cfa46", "jest-image-snapshot": "^4.5.1", "jest-junit": "^13.0.0", "lint-staged": "^12.1.2", @@ -72,6 +72,7 @@ "react-intl": "^5.20.12", "react-json-view": "^1.21.3", "react-multi-provider": "^0.1.5", + "react-page-visibility": "^6.4.0", "react-router-dom": "^6.0.2", "react-scripts": "5.0.0", "react-text-mask": "^5.4.3", @@ -92,6 +93,7 @@ }, "scripts": { "start": "export NODE_OPTIONS=--openssl-legacy-provider && craco start --openssl-legacy-provider", + "start:rococo": "REACT_APP_NODE_URL=wss://rpc-01.basilisk-rococo.hydradx.io craco start", "build": "export NODE_OPTIONS=--openssl-legacy-provider && craco build", "build:deployment": "export REACT_APP_GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) && craco build", "test": "craco test", @@ -209,6 +211,7 @@ "@storybook/react": "^6.3.11", "@testing-library/react-hooks": "^7.0.2", "@types/lodash": "^4.14.177", + "@types/react-page-visibility": "^6.4.1", "babel-plugin-formatjs": "^10.3.9", "bignumber.js": "^9.0.1", "chai": "^4.3.4", diff --git a/src/App.scss b/src/App.scss index d1ecdac9..90cba4d6 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,5 +1,12 @@ @import './misc/colors.module.scss'; +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + .debug { display: none; @@ -16,28 +23,125 @@ color: $red1; } +.trade-modal-component-wrapper { + position: relative; + display: flex; + flex-direction: column; + align-content: space-between; + padding: 0; + background: linear-gradient(180deg, #1c2527 0%, #14161a 80.73%, #121316 100%); + + .modal-component-heading { + display: flex; + justify-content: center; + width: 100%; + + color: $l-gray3; + text-transform: capitalize; + align-items: flex-start; + + padding: 24px 0; + + &__main-text { + font-size: 16px; + font-weight: 500; + width: fit-content; + } + } + + .close-modal-btn { + position: absolute; + left: 16px; + top: 20px; + display: flex; + align-items: center; + justify-content: center; + } + + .close-modal-btn:hover { + cursor: pointer; + + svg { + path { + fill: $orange1; + } + } + } +} + .modal-component-wrapper { position: relative; display: flex; flex-direction: column; align-content: space-between; padding-bottom: 8px; - gap: 16px; + gap: 24px; + + padding: 30px; + border: 1px solid #29292d; + + background: #211f24; + box-shadow: 0px 35px 71px -47px rgba(82, 255, 177, 0.37); + border-radius: 16px; - background-color: $d-gray6; overflow: hidden; + &::before { + content: ' '; + width: 100%; + height: 100%; + top: 0; + left: 0; + position: fixed; + backdrop-filter: blur(3px); + background: radial-gradient( + 70.22% 56.77% at 51.87% 101.05%, + rgba(79, 255, 176, 0.24) 0%, + rgba(79, 255, 176, 0) 100% + ), + rgba(7, 8, 14, 0.7); + z-index: -1; + } + .modal-component-heading { display: flex; justify-content: space-between; width: 100%; + padding-top: 4px; + color: $l-gray3; + text-transform: capitalize; + align-items: center; - background-color: $d-gray4; - font-size: 14px; - padding: 14px; - font-weight: 600; - text-transform: uppercase; - color: $green1; + &__main-text { + font-size: 22px; + font-weight: 500; + background: linear-gradient( + 90deg, + #4fffb0 1.27%, + #b3ff8f 48.96%, + #ff984e 104.14% + ), + linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%), + linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + width: fit-content; + + &__secondary { + font-size: 16px; + color: #d1dee8; + text-transform: none; + background: none; + + padding: 8px 0; + + -webkit-background-clip: none; + -webkit-text-fill-color: #d1dee8; + background-clip: none; + } + } } .close-modal-btn { @@ -59,13 +163,13 @@ .modal-component-content { display: flex; flex-direction: column; - gap: 8px; + gap: 16px; flex-grow: 1; + color: white; - overflow-y: scroll; - margin: 0 8px; + overflow-y: auto; - padding: 0 8px; + padding: 0 16px 16px 0; &::-webkit-scrollbar { width: 6px; @@ -74,6 +178,7 @@ /* Track */ &::-webkit-scrollbar-track { background-color: $gray3; + margin-bottom: 30px; } /* Handle */ @@ -91,4 +196,4 @@ letter-spacing: 0.5px; line-height: 1.2em; -} \ No newline at end of file +} diff --git a/src/compiled-lang/en.json b/src/compiled-lang/en.json index 0babdb95..c92a76e1 100644 --- a/src/compiled-lang/en.json +++ b/src/compiled-lang/en.json @@ -9,9 +9,9 @@ "Wallet.InstallInstructions": "To connect your account, please {link} ", "Wallet.InstallLinkText": "install or enable", "Wallet.Loading": "Loading...", - "Wallet.NoAccountsAvailable": "You have no accounts available", + "Wallet.NoAccountsAvailable": "You have no accounts available. ", "Wallet.ReloadInstructions": "the polkadot.js extension. Once you're done with the installation, you can {link}", "Wallet.ReloadLinkText": "reload the page", - "Wallet.SelectAccount": "Select an account", - "Wallet.SelectAccountHelp": "Do you need help creating an account?" + "Wallet.SelectAccount": "Select account", + "Wallet.SelectAccountHelp": "Do you need help creating an account? {link}" } diff --git a/src/components/Balance/AssetBalanceInput/AssetBalanceInput.scss b/src/components/Balance/AssetBalanceInput/AssetBalanceInput.scss index c9dd3bc7..92961a6e 100644 --- a/src/components/Balance/AssetBalanceInput/AssetBalanceInput.scss +++ b/src/components/Balance/AssetBalanceInput/AssetBalanceInput.scss @@ -6,21 +6,22 @@ position: relative; align-items: center; - height: 60px; + width: 100%; - background-color: $d-gray2; border-radius: $border-radius; .balance-input { height: 20px; border-radius: 0; + + input { + background: transparent; + } } &__asset-info { height: 100%; - padding: 16px 12px; - display: flex; align-items: center; gap: 8px; @@ -76,13 +77,16 @@ } } } - - box-shadow: 1px 0 $d-gray4; } + box-shadow: 1px 0 $d-gray4; + &__input-wrapper { flex-grow: 1; padding: 16px 12px; + background-color: rgba(218, 255, 238, 0.06); + border-radius: $border-radius; + box-shadow: 0 0 0 1px rgba(255, 255, 238, 0.3); &__unit-selector { display: flex; diff --git a/src/components/Balance/AssetBalanceInput/AssetBalanceInput.tsx b/src/components/Balance/AssetBalanceInput/AssetBalanceInput.tsx index 3f0ad6c0..5cfefda6 100644 --- a/src/components/Balance/AssetBalanceInput/AssetBalanceInput.tsx +++ b/src/components/Balance/AssetBalanceInput/AssetBalanceInput.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; -import { MutableRefObject, useCallback } from 'react'; -import { Asset } from '../../../generated/graphql'; +import { MutableRefObject, useCallback, useEffect } from 'react'; +import { Asset, Maybe } from '../../../generated/graphql'; import { BalanceInput, BalanceInputProps } from '../BalanceInput/BalanceInput'; import { useModalPortal } from './hooks/useModalPortal'; import { useModalPortalElement } from './hooks/useModalPortalElement'; @@ -24,7 +24,9 @@ export interface AssetBalanceInputProps { isAssetSelectable?: boolean; // onAssetSelected: (asset: Asset) => void, balanceInputRef?: MutableRefObject; - required?: boolean + required?: boolean; + disabled?: boolean; + maxBalanceLoading?: boolean, } export const AssetBalanceInput = ({ @@ -38,6 +40,8 @@ export const AssetBalanceInput = ({ // onAssetSelected, balanceInputRef, required, + disabled, + maxBalanceLoading, }: AssetBalanceInputProps) => { const modalPortalElement = useModalPortalElement({ assets, @@ -58,44 +62,64 @@ export const AssetBalanceInput = ({ const methods = useFormContext(); return ( -
+
{/* This portal will be rendered at it's container ref as defined above */} {modalPortal} -
handleAssetSelectorClick()} - data-modal-portal-toggle={toggleId} - > -
-
-
- {idToAsset(methods.getValues(assetInputName))?.fullName || - 'Select asset'} -
-
- {idToAsset(methods.getValues(assetInputName))?.symbol || - methods.getValues(assetInputName) || - '---'} - + {isAssetSelectable + ? ( +
handleAssetSelectorClick()} + data-modal-portal-toggle={toggleId} + > +
+
+
+ {isAssetSelectable + ? (idToAsset(methods.getValues(assetInputName))?.fullName || + 'Select asset') + : 'No asset' + } +
+
+ {idToAsset(methods.getValues(assetInputName))?.symbol || + methods.getValues(assetInputName) || + '---'} + {isAssetSelectable && } +
+
-
-
+ ) + : <> + }
-
- -
- {idToAsset(methods.getValues(assetInputName))?.symbol || `${horizontalBar}`} -
-
+
+
+ +
+ {idToAsset(methods.getValues(assetInputName))?.symbol || + `${horizontalBar}`} +
+
+
diff --git a/src/components/Balance/AssetBalanceInput/AssetSelector/AssetItem/AssetItem.tsx b/src/components/Balance/AssetBalanceInput/AssetSelector/AssetItem/AssetItem.tsx index 784d28ed..bca6941a 100644 --- a/src/components/Balance/AssetBalanceInput/AssetSelector/AssetItem/AssetItem.tsx +++ b/src/components/Balance/AssetBalanceInput/AssetSelector/AssetItem/AssetItem.tsx @@ -1,6 +1,8 @@ import { Asset } from '../../../../../generated/graphql'; import classNames from 'classnames'; import { idToAsset } from '../../../../../pages/TradePage/TradePage'; +import { horizontalBar } from '../../../../Chart/ChartHeader/ChartHeader'; +import Unknown from '../../../../../misc/icons/assets/Unknown.svg'; export interface AssetItemProps { asset: Asset; onClick: () => void; @@ -21,12 +23,12 @@ export const AssetItem = ({ asset, onClick, active }: AssetItemProps) => (
- {idToAsset(asset.id)?.fullName || ''} + {idToAsset(asset.id)?.fullName}
{idToAsset(asset.id)?.symbol} diff --git a/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.scss b/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.scss index 376e4bc6..436ffbbc 100644 --- a/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.scss +++ b/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.scss @@ -18,8 +18,6 @@ height: 100%; &__asset-item { - height: 50px; - border: 1px solid transparent; border-radius: $border-radius; @@ -39,7 +37,7 @@ } cursor: pointer; - padding: 0.5em 0; + padding: 16px; &:hover { color: $green1; diff --git a/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.tsx b/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.tsx index 9b0a2cf7..07b9bd00 100644 --- a/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.tsx +++ b/src/components/Balance/AssetBalanceInput/AssetSelector/AssetSelector.tsx @@ -31,28 +31,25 @@ export const AssetSelector = ({
-
Select an asset
{' '} +
Select asset
{' '}
- {assets?.length - ? ( - assets?.map((asset, i) => ( - onAssetSelected(asset)} - active={asset.id === activeAsset?.id} - asset={asset} - /> - )) - ) - : ( -

No other assets available

- ) - } + {assets?.length ? ( + assets?.map((asset, i) => ( + onAssetSelected(asset)} + active={asset.id === activeAsset?.id} + asset={asset} + /> + )) + ) : ( +

No other assets available

+ )}
diff --git a/src/components/Balance/AssetBalanceInput/hooks/useModalPortal.tsx b/src/components/Balance/AssetBalanceInput/hooks/useModalPortal.tsx index 5d824b8a..c7c8537f 100644 --- a/src/components/Balance/AssetBalanceInput/hooks/useModalPortal.tsx +++ b/src/components/Balance/AssetBalanceInput/hooks/useModalPortal.tsx @@ -2,35 +2,80 @@ import { MutableRefObject, ReactNode, ReactPortal, useCallback, useEffect, useMe import { createPortal } from 'react-dom'; import { useOnClickOutside } from 'use-hooks'; import { v4 as uuidv4 } from 'uuid'; -export interface ModalPortalElementFactoryArgs { +export interface ModalPortalElementFactoryArgs { openModal: () => void, closeModal: () => void, toggleModal: () => void, + resolve: (value?: any) => void, + reject: (value?: any) => void, + cancel: (value?: any) => void, elementRef: MutableRefObject, isModalOpen: boolean, + state?: T } -export type ModalPortalElementFactory = (args: ModalPortalElementFactoryArgs) => ReactNode; +export type ModalPortalElementFactory = (args: ModalPortalElementFactoryArgs) => ReactNode; -export const useModalPortal = ( - elementFactory: ModalPortalElementFactory, +export const useModalPortal = ( + elementFactory: ModalPortalElementFactory, container: MutableRefObject, closeOnClickOutside: boolean = true, ) => { const [modalPortal, setModalPortal] = useState(); const [isModalOpen, setIsModalOpen] = useState(false); + const [status, setStatus] = useState<'pending' | 'success' | 'failure' | 'cancelled'>('pending'); + const resolve = useCallback(() => { + setStatus('success'); + setIsModalOpen(false); + }, []); + const reject = useCallback(() => { + setStatus('failure'); + setIsModalOpen(false); + }, []); + const cancel = useCallback(() => { + setStatus('cancelled'); + setIsModalOpen(false); + }, []); + + // old components might still use toggle/open/close API const toggleModal = useCallback(() => setIsModalOpen(isModalOpen => !isModalOpen), [setIsModalOpen]); - const openModal = useCallback(() => setIsModalOpen(true), [setIsModalOpen]); - const closeModal = useCallback(() => setIsModalOpen(false), [setIsModalOpen]); + + const closeModal = useCallback(() => { + setIsModalOpen(false); + setStatus('cancelled'); + }, []); + + const [state, setState] = useState(); + + const openModal = useCallback((state?: any) => { + state && setState(state); + setStatus('pending'); + + setIsModalOpen(true) + }, [setIsModalOpen, setState]); const elementRef = useRef(null); const toggleId = useMemo(() => uuidv4(), []); const element = useMemo(() => { - return elementFactory({ toggleModal, openModal, closeModal, elementRef, isModalOpen }) - }, [elementFactory, toggleModal, openModal, closeModal, isModalOpen, elementRef]); +// <<<<<<< HEAD +// return elementFactory({ +// toggleModal, +// openModal, +// closeModal, +// elementRef, +// isModalOpen, +// resolve, +// reject, +// cancel +// }) +// }, [elementFactory, toggleModal, openModal, closeModal, isModalOpen, elementRef, resolve, reject, cancel]); +// ======= + return elementFactory({ toggleModal, openModal, closeModal, elementRef, isModalOpen, state, resolve, reject, cancel }) + }, [elementFactory, toggleModal, openModal, closeModal, isModalOpen, elementRef, state]); +// >>>>>>> 28bc535d48f7808dcf3e723f0952d438799a3209 useEffect(() => { if (!container.current || !element) return; @@ -50,6 +95,7 @@ export const useModalPortal = ( closeModal, isModalOpen, toggleId, + status, modalPortal: modalPortal }; } \ No newline at end of file diff --git a/src/components/Balance/AssetBalanceInput/hooks/useModalPortalElement.tsx b/src/components/Balance/AssetBalanceInput/hooks/useModalPortalElement.tsx index dda0cd71..ec088ec1 100644 --- a/src/components/Balance/AssetBalanceInput/hooks/useModalPortalElement.tsx +++ b/src/components/Balance/AssetBalanceInput/hooks/useModalPortalElement.tsx @@ -16,7 +16,7 @@ export type ModalPortalElement = ({ AssetBalanceInputProps, 'assets' | 'defaultAsset' | 'assetInputName' >) => ModalPortalElementFactory; -export type CloseModal = ModalPortalElementFactoryArgs['closeModal']; +export type CloseModal = ModalPortalElementFactoryArgs['closeModal']; export const useModalPortalElement: ModalPortalElement = ({ assets, diff --git a/src/components/Balance/BalanceInput/BalanceInput.scss b/src/components/Balance/BalanceInput/BalanceInput.scss index 1e8130bd..f788ea2b 100644 --- a/src/components/Balance/BalanceInput/BalanceInput.scss +++ b/src/components/Balance/BalanceInput/BalanceInput.scss @@ -22,6 +22,7 @@ position: absolute; font-size: 1em; line-height: 1em; + font-weight: 500; padding: 0; text-align: right; diff --git a/src/components/Balance/BalanceInput/BalanceInput.tsx b/src/components/Balance/BalanceInput/BalanceInput.tsx index 4ef42457..0c596724 100644 --- a/src/components/Balance/BalanceInput/BalanceInput.tsx +++ b/src/components/Balance/BalanceInput/BalanceInput.tsx @@ -34,7 +34,8 @@ export interface BalanceInputProps { * retrieve the actual input element from the form state */ inputRef?: MutableRefObject; - required?: boolean + required?: boolean; + disabled?: boolean } const MaskedInputWithRef = React.forwardRef( @@ -80,7 +81,8 @@ export const BalanceInput = ({ defaultUnit = MetricUnit.NONE, showMetricUnitSelector = true, inputRef, - required + required, + disabled }: BalanceInputProps) => { const { control, register, setValue, getValues, watch } = useFormContext(); const { unit, setUnit } = useDefaultUnit(defaultUnit); @@ -123,6 +125,7 @@ export const BalanceInput = ({ required={required} onChange={(e) => handleOnChange(field, e)} placeholder="0.00" + disabled={disabled} /> )} diff --git a/src/components/Balance/BalanceInput/MetricUnitSelector/MetricUnitSelector.scss b/src/components/Balance/BalanceInput/MetricUnitSelector/MetricUnitSelector.scss index fc44e571..4341379c 100644 --- a/src/components/Balance/BalanceInput/MetricUnitSelector/MetricUnitSelector.scss +++ b/src/components/Balance/BalanceInput/MetricUnitSelector/MetricUnitSelector.scss @@ -4,18 +4,19 @@ .metric-unit-selector { position: relative; - display: flex; + display: none; flex-shrink: 1; justify-content: right; font-weight: 600; letter-spacing: 0.5px; + font-size: 10px; min-width: 100px; &:hover { cursor: pointer; - + svg { path { fill: $green1; @@ -25,7 +26,7 @@ &__select { display: flex; - font-size: 0.65em; + font-size: 10px; justify-content: center; align-items: center; @@ -66,7 +67,7 @@ z-index: 1; - font-size: 0.65em; + font-size: 10px; overflow: hidden; diff --git a/src/components/Balance/FormattedBalance/FormattedBalance.scss b/src/components/Balance/FormattedBalance/FormattedBalance.scss index 3f9fe644..e00c0f9d 100644 --- a/src/components/Balance/FormattedBalance/FormattedBalance.scss +++ b/src/components/Balance/FormattedBalance/FormattedBalance.scss @@ -7,7 +7,7 @@ justify-self: end; justify-content: right; - font-weight: 600; + font-weight: 400; letter-spacing: 0.5px; line-height: 1.2em; diff --git a/src/components/Balance/FormattedBalance/FormattedBalance.tsx b/src/components/Balance/FormattedBalance/FormattedBalance.tsx index 49bab7b1..54ffcac5 100644 --- a/src/components/Balance/FormattedBalance/FormattedBalance.tsx +++ b/src/components/Balance/FormattedBalance/FormattedBalance.tsx @@ -7,6 +7,8 @@ import { useFormatSI } from './hooks/useFormatSI'; import { idToAsset } from '../../../pages/TradePage/TradePage'; import ReactTooltip from 'react-tooltip'; import { fromPrecision12 } from '../../../hooks/math/useFromPrecision'; +import { horizontalBar } from '../../Chart/ChartHeader/ChartHeader'; +import BigNumber from 'bignumber.js'; export interface FormattedBalanceProps { balance: Balance; @@ -16,44 +18,67 @@ export interface FormattedBalanceProps { export const FormattedBalance = ({ balance, - precision = 2, + precision = 3, unitStyle = UnitStyle.LONG, }: FormattedBalanceProps) => { const assetSymbol = useMemo(() => idToAsset(balance.assetId)?.symbol, [ balance.assetId, ]); - const formattedBalance = useFormatSI(precision, unitStyle, balance.balance); + // const formattedBalance = useFormatSI(precision, unitStyle, balance.balance); + let formattedBalance = fromPrecision12(balance.balance); + + const decimalPlacesCount = formattedBalance?.split('.')[1]?.length || 0; + console.log('formattedBalance', decimalPlacesCount, formattedBalance ) + + if (formattedBalance && new BigNumber(formattedBalance).gte(1)) { + formattedBalance = new BigNumber(formattedBalance).toFixed( + decimalPlacesCount > 4 ? 4 : decimalPlacesCount + ); + } else if (formattedBalance) { + formattedBalance = new BigNumber(formattedBalance).toFixed( + decimalPlacesCount <= 4 ? 4 : decimalPlacesCount + ); + } + const tooltipText = useMemo(() => { // TODO: get rid of raw html return ` ${fromPrecision12(balance.balance)} ${assetSymbol} - ` + `; }, [balance, assetSymbol]); useEffect(() => { ReactTooltip.rebuild(); }, [tooltipText]); - log.debug( - 'FormattedBalance', - formattedBalance?.value, - formattedBalance?.unit, - formattedBalance?.numberOfDecimalPlaces - ); + // log.debug( + // 'FormattedBalance', + // formattedBalance?.value, + // formattedBalance?.unit, + // formattedBalance?.numberOfDecimalPlaces + // ); // We don't need to use the currency input here // because when there is more than 3 significant digits, the formatter // moves one notch up/down and keeps a fixed precision return ( // WARNING POSSIBLY UNSAFE?? -
-
{formattedBalance.value}
-
+
+ {/*
{formattedBalance.value}
*/} +
{formattedBalance}
+ {/*
{formattedBalance.suffix} +
*/} +
+ {assetSymbol || horizontalBar}
-
{assetSymbol}
); }; diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss index c486a1a7..e4d5096c 100644 --- a/src/components/Button/Button.scss +++ b/src/components/Button/Button.scss @@ -13,20 +13,47 @@ width: 100%; &--primary { - padding: 0.875rem; - background: $green1; - text-transform: uppercase; + height: 40px; + user-select: none; + border-radius: 9999px; + width: fit-content; + background-color: #4fffb0; + color: #26282f; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border: none; + &:hover { - background: darken($color: $green1, $amount: 5); + background-color: #41db96; + } + + .label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 8px 16px; + font-size: 16px; + line-height: 16px; + font-weight: 600; } } &--secondary { - padding: 0.3125rem; - color: $white1; - background: $gray3; + height: 40px; + user-select: none; + border-radius: 9999px; + width: fit-content; + padding: 8px 16px; + color: $gray4; + background: none; + display: flex; + justify-content: center; + &:hover { - background: darken($color: $green1, $amount: 5); + color: $green1; } } diff --git a/src/components/Chart/ChartHeader/ChartHeader.scss b/src/components/Chart/ChartHeader/ChartHeader.scss index fef53765..25d80f62 100644 --- a/src/components/Chart/ChartHeader/ChartHeader.scss +++ b/src/components/Chart/ChartHeader/ChartHeader.scss @@ -59,7 +59,8 @@ $disabledOpacity: 0.3; &__pool-info { display: flex; - justify-content: space-between; + justify-content: start; + padding-top: 36px; &__assets { display: flex; diff --git a/src/components/Confirmation/Confirmation.scss b/src/components/Confirmation/Confirmation.scss new file mode 100644 index 00000000..aeeb0ff9 --- /dev/null +++ b/src/components/Confirmation/Confirmation.scss @@ -0,0 +1,25 @@ +.confirmation-screen { + position: fixed; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + + background: rgba(50, 50, 50, 0.5); + + z-index: 3; + + .modal-component-wrapper { + width: 400px; + min-height: 300px; + max-height: 500px; + } + + .buttons { + display: flex; + justify-content: space-between; + } +} diff --git a/src/components/Confirmation/Confirmation.tsx b/src/components/Confirmation/Confirmation.tsx new file mode 100644 index 00000000..0133d120 --- /dev/null +++ b/src/components/Confirmation/Confirmation.tsx @@ -0,0 +1,50 @@ +import { ConfirmationType } from '../../hooks/actionLog/useWithConfirmation'; +import { SubmitTradeMutationVariables } from '../../hooks/pools/mutations/useSubmitTradeMutation'; +import { ModalPortalElementFactoryArgs } from '../Balance/AssetBalanceInput/hooks/useModalPortal'; +import './Confirmation.scss'; + +export const Confirmation = ({ + isModalOpen, + options, + resolve, + reject, + confirmationType, +}: ModalPortalElementFactoryArgs & { + options?: SubmitTradeMutationVariables; // or any other type that might be handled through confirmations + confirmationType: ConfirmationType; +}) => { + + console.log('options', options); + + return isModalOpen ? ( +
+
+
+
+ Confirm transaction +
+
+
+

Trade type: {options?.tradeType}

+

+ Assets: {options?.assetInId} / {options?.assetOutId} +

+

+ Amounts: {options?.assetInAmount} / {options?.assetOutAmount} +

+

Limit: {options?.amountWithSlippage}

+
+
+ + +
+
+
+ ) : ( + <> + ); +}; diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx index bf0f3291..36c26e5a 100644 --- a/src/components/Icon/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -4,6 +4,7 @@ import { ReactComponent as NotificationActiveIcon } from './assets/NotificationA import { ReactComponent as NotificationInactiveIcon } from './assets/NotificationInactiveIcon.svg'; import { ReactComponent as DropdownArrowIcon } from './assets/DropdownArrowIcon.svg'; import { ReactComponent as CancelIcon } from './assets/Cancel.svg'; +import { ReactComponent as BackIcon } from './assets/Back.svg'; import { ReactComponent as BasiliskLogoFull } from './assets/BasiliskLogoFull.svg'; import { ReactComponent as AssetSwitchIcon } from './assets/AssetSwitchIcon.svg'; import { ReactComponent as SettingsIcon } from './assets/Settings.svg'; @@ -15,6 +16,7 @@ const Icons = { NotificationInactive: () => , DropdownArrow: () => , Cancel: () => , + Back: () => , BasiliskLogoFull: () => , AssetSwitch: () => , Settings: () => , diff --git a/src/components/Icon/assets/AssetSwitchIcon.svg b/src/components/Icon/assets/AssetSwitchIcon.svg index 06067778..95e1c092 100644 --- a/src/components/Icon/assets/AssetSwitchIcon.svg +++ b/src/components/Icon/assets/AssetSwitchIcon.svg @@ -1,16 +1,20 @@ diff --git a/src/components/Icon/assets/Back.svg b/src/components/Icon/assets/Back.svg new file mode 100644 index 00000000..10eb8514 --- /dev/null +++ b/src/components/Icon/assets/Back.svg @@ -0,0 +1,19 @@ + + + + diff --git a/src/components/Navigation/ActionBar.tsx b/src/components/Navigation/ActionBar.tsx index e994b23a..ee4b3e1e 100644 --- a/src/components/Navigation/ActionBar.tsx +++ b/src/components/Navigation/ActionBar.tsx @@ -2,13 +2,13 @@ import './ActionBar.scss'; import { Link } from 'react-router-dom'; export interface ActionBarProps { - isExtensionAvailable: boolean; + isExtensionAvailable: boolean; extensionLoading: boolean; activeAccountLoading: boolean; accountData?: { - name?: string; - address?: string; - nativeAssetBalance?: string; + name?: string; + address?: string; + nativeAssetBalance?: string; }; } @@ -20,34 +20,41 @@ export const ActionBar = ({ }: ActionBarProps) => { return (
-
+
?
!
{extensionLoading || activeAccountLoading ? ( -
loading...
+
loading...
) : isExtensionAvailable ? ( <> {accountData?.name ? ( -
-
- {accountData?.nativeAssetBalance} BSX -
- {/* TODO! Acc name / address + Icon component*/} -
- {accountData?.name} -
+
+
+ {accountData?.nativeAssetBalance} BSX
+ {/* TODO! Acc name / address + Icon component*/} +
+ {accountData?.name} +
+
) : ( - select an account + + select account + )} ) : ( -
Extension unavailable
+
+ Extension unavailable +
)}
-
v
+
v
); }; diff --git a/src/components/Pools/PoolsForm.scss b/src/components/Pools/PoolsForm.scss new file mode 100644 index 00000000..8895eda0 --- /dev/null +++ b/src/components/Pools/PoolsForm.scss @@ -0,0 +1,334 @@ +@import './../../misc/colors.module.scss'; +@import './../../misc/misc.module.scss'; +@import '../Button/Button.scss'; + +.pools-form-wrapper { + position: relative; + flex-basis: 350px; + flex-grow: 1; + + padding: 22px; + min-width: 350px; + max-width: 610px; + margin: 0 auto; + + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05); + background: linear-gradient(180deg, #1c2527 0%, #14161a 80.73%, #121316 100%); + overflow: hidden; + border-radius: 10px; + + position: relative; + + color: white; + + .settings-button-wrapper { + position: absolute; + display: flex; + flex-direction: row; + justify-content: left; + right: 10px; + top: 10px; + gap: 10px; + padding: 10px; + + .pool-settings-button { + display: flex; + padding: 10px 8px; + width: fit-content; + height: fit-content; + border-radius: 50%; + background-color: rgba(162, 176, 187, 0.1); + + svg { + width: 24px; + } + + &:hover { + cursor: pointer; + + svg { + path { + fill: $green1; + } + } + } + } + + .pool-page-tabs { + display: flex; + flex-direction: row; + justify-content: right; + width: 90%; + + .tab { + @extend .button--primary; + width: 100px; + border-radius: $border-radius 0px 0px $border-radius; + color: $gray4; + background-color: rgba(162, 176, 187, 0.1); + + &:hover { + color: rgba(79, 255, 176, 1); + background-color: rgba(162, 176, 187, 0.15); + } + + &:disabled { + color: rgba(79, 255, 176, 1); + background-color: rgba(162, 176, 187, 0.2); + } + + &:first-child { + border-radius: $border-radius 0px 0px $border-radius; + } + + &:last-child { + border-radius: 0px $border-radius $border-radius 0px; + } + + &:not(:last-child) { + border-right: 1px solid rgba(162, 176, 187, 0.1); + } + } + } + } + + .pools-form { + height: 100%; + min-height: 400px; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 14px; + + .pools-form-heading { + width: fit-content; + padding-top: 4px; + color: $l-gray3; + font-size: 22px; + font-weight: 500; + background: linear-gradient( + 90deg, + #4fffb0 1.27%, + #b3ff8f 48.96%, + #ff984e 104.14% + ), + linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%), + linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + .divider-wrapper { + display: flex; + align-items: center; + height: 1px; + width: 100%; + } + + .divider { + position: absolute; + width: 100%; + height: 1px; + background-color: rgba(76, 243, 168, 0.12); + opacity: 1; + border: 0; + left: 0; + } + + .balance-wrapper { + display: flex; + flex-direction: column-reverse; + align-items: end; + background: rgba(162, 176, 187, 0.1); + padding: 12px; + padding-top: 24px; + border-radius: 10px; + gap: 6px; + padding-bottom: 16px; + } + + .balance-wrapper-share-tokens { + @extend .balance-wrapper; + margin-top: 8px; + margin-bottom: 8px; + } + + .submit-button { + background: $green1; + text-transform: uppercase; + border-radius: 36px; + height: 50px; + + color: $d-gray4; + + &:hover { + background-color: $green2; + } + + &:disabled { + background-color: $l-gray5; + } + } + } +} + +// SHOULD BE EXTRACTED TO COMPONENTS + +.balance-info { + display: flex; + align-items: center; + justify-content: right; + width: 100%; + gap: 4px; + + height: 16px; + margin-top: 4px; + font-size: 12px; + line-height: 12px; + position: relative; + + .balance-info-type { + position: absolute; + left: 0; + top: -7px; + font-weight: 600; + font-size: 16px; + color: $green1; + padding: 6px; + } +} + +.asset-switch { + display: flex; + height: 43px; + justify-content: space-between; + align-items: center; + width: 100%; + + .asset-switch-icon { + position: absolute; + left: 24px; + + display: flex; + align-items: center; + justify-content: center; + + overflow: hidden; + background: #192022; + border-radius: 50%; + + transition: transform 500ms ease; + + &:hover { + cursor: pointer; + + transform: rotate(180deg); + + svg { + path { + fill: $green1; + } + } + } + } + + .asset-switch-price { + position: absolute; + right: 24px; + background: #192022; + + &__wrapper { + display: flex; + align-items: center; + gap: 4px; + + padding: 4px 14px; + font-size: 11px; + font-weight: 500; + + background: rgba(218, 255, 238, 0.06); + border-radius: 7px; + } + } +} + +.trade-settings-wrapper { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + + z-index: 1; + + .disclaimer { + padding: 12px 24px; + padding-top: 0px; + font-size: 14px; + color: $gray4; + } + + .trade-settings { + height: 100%; + } + + .settings-section { + padding: 12px 24px; + background: linear-gradient(0deg, #171518, #171518), #1c1a1f; + } + + .settings-field { + padding: 12px 24px; + display: flex; + justify-content: space-between; + align-items: center; + + &__label { + flex-grow: 10; + } + + input { + flex-shrink: 10; + flex-basis: 50px; + width: 50px; + text-align: center; + + border-radius: $border-radius; + } + } + + &.hidden { + display: none; + } +} + +.debug-box { + position: fixed; + padding: 16px; + right: 0; + top: 0; + + height: 100%; + + overflow-y: scroll; + + background-color: rgba(0, 0, 0, 0.8); +} + +.max-button { + font-size: 12px; + font-weight: 400; + color: $white1; + padding: 4px 10px; + background: rgba(255, 255, 255, 0.06); + border-radius: 12px; + text-transform: capitalize; + cursor: pointer; + + &.disabled { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/src/components/Pools/PoolsForm.tsx b/src/components/Pools/PoolsForm.tsx new file mode 100644 index 00000000..5da745db --- /dev/null +++ b/src/components/Pools/PoolsForm.tsx @@ -0,0 +1,1261 @@ +import BigNumber from 'bignumber.js'; +import classNames from 'classnames'; +import { every, find, times } from 'lodash'; +import { + MutableRefObject, + useCallback, + useDebugValue, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Control, FormProvider, useForm } from 'react-hook-form'; +import { Account, Balance, Maybe, Pool } from '../../generated/graphql'; +import { fromPrecision12 } from '../../hooks/math/useFromPrecision'; +import { useMath } from '../../hooks/math/useMath'; +import { percentageChange } from '../../hooks/math/usePercentageChange'; +import { toPrecision12 } from '../../hooks/math/useToPrecision'; +import { SubmitTradeMutationVariables } from '../../hooks/pools/mutations/useSubmitTradeMutation'; +import { idToAsset, TradeAssetIds } from '../../pages/TradePage/TradePage'; +import { AssetBalanceInput } from '../Balance/AssetBalanceInput/AssetBalanceInput'; +import { PoolType } from '../Chart/shared'; +import { PoolsInfo } from './PoolsInfo/PoolsInfo'; +import './PoolsForm.scss'; +import Icon from '../Icon/Icon'; +import { useModalPortal } from '../Balance/AssetBalanceInput/hooks/useModalPortal'; +import { FormattedBalance } from '../Balance/FormattedBalance/FormattedBalance'; +import { useDebugBoxContext } from '../../pages/TradePage/hooks/useDebugBox'; +import { horizontalBar } from '../Chart/ChartHeader/ChartHeader'; +import { usePolkadotJsContext } from '../../hooks/polkadotJs/usePolkadotJs'; +import { useApolloClient } from '@apollo/client'; +import { estimateBuy } from '../../hooks/pools/xyk/buy'; +import { estimateSell } from '../../hooks/pools/xyk/sell'; +import { payment } from '@polkadot/types/interfaces/definitions'; +import { useMultiFeePaymentConversionContext } from '../../containers/MultiProvider'; + +export interface PoolsFormSettingsProps { + allowedSlippage: string | null; + onAllowedSlippageChange: (allowedSlippage: string | null) => void; + closeModal: any; +} + +export enum ProvisioningType { + Add, + Remove, +} + +export interface PoolsFormSettingsFormFields { + allowedSlippage: string | null; + autoSlippage: boolean; +} + +export const PoolsFormSettings = ({ + allowedSlippage, + onAllowedSlippageChange, + closeModal, +}: PoolsFormSettingsProps) => { + const { register, watch, getValues, setValue, handleSubmit } = + useForm({ + defaultValues: { + allowedSlippage, + autoSlippage: true, + }, + }); + + // propagate allowed slippage to the parent + useEffect(() => { + onAllowedSlippageChange(getValues('allowedSlippage')); + }, watch(['allowedSlippage'])); + + // if you want automatic slippage, override the previous user's input + useEffect(() => { + if (getValues('autoSlippage')) { + // default is 3% + setValue('allowedSlippage', '3'); + } + }, watch(['autoSlippage'])); + + return ( +
{})} + > +
+
Settings
+
+ +
+
+
+
Liquidity provisioning
+ + +
+ The deviation of the final acceptable price from the spot price caused by the change in price between announcing the transaction and processing it. +
+
+
+ ); +}; + +export const useModalPortalElement = ({ + allowedSlippage, + setAllowedSlippage, +}: any) => { + return useCallback( + ({ closeModal, elementRef, isModalOpen }) => { + return ( +
+ { + setAllowedSlippage(allowedSlippage); + }} + /> +
+ ); + }, + [allowedSlippage] + ); +}; + +export interface PoolsFormProps { + assets?: { id: string }[]; + assetIds: TradeAssetIds; + onAssetIdsChange: (assetIds: TradeAssetIds) => void; + isActiveAccountConnected?: boolean; + pool?: Pool; + assetInLiquidity?: string; + assetOutLiquidity?: string; + spotPrice?: { + outIn?: string; + inOut?: string; + }; + isPoolLoading: boolean; + onSubmit: (form: PoolsFormFields & { amountBMaxLimit?: string }) => void; + tradeLoading: boolean; + activeAccountTradeBalances?: { + outBalance?: Balance; + inBalance?: Balance; + shareBalance?: Balance; + }; + activeAccountTradeBalancesLoading: boolean; + activeAccount?: Maybe; +} + +export interface PoolsFormFields { + assetIn: string | null; + assetOut: string | null; + assetInAmount: string | null; + assetOutAmount: string | null; + shareAssetAmount: string | null; + submit: void; + warnings: any; + provisioningType: ProvisioningType; +} + +/** + * Trigger a state update each time the given input changes (via the `input` event) + * @param control + * @param field + * @returns + */ +export const useListenForInput = ( + inputRef: MutableRefObject +) => { + const [state, setState] = useState(); + + useEffect(() => { + if (!inputRef) return; + // TODO: figure out why using the 'input' broke the mask + // 'keydown' also doesnt work bcs its triggered by copy/paste, which then + // changes the trade type (which this hook is primarily) + const listener = inputRef.current?.addEventListener('keydown', () => + setState((state) => !state) + ); + + return () => + listener && inputRef.current?.removeEventListener('keydown', listener); + }, [inputRef]); + + return state; +}; + +export const PoolsForm = ({ + assetIds, + onAssetIdsChange, + isActiveAccountConnected, + pool, + isPoolLoading, + assetInLiquidity, + assetOutLiquidity, + spotPrice, + onSubmit, + tradeLoading, + assets, + activeAccountTradeBalances, + activeAccountTradeBalancesLoading, + activeAccount, +}: PoolsFormProps) => { + // TODO: include math into loading form state + const { math, loading: mathLoading } = useMath(); + const [provisioningType, setProvisioningType] = useState( + ProvisioningType.Add + ); + const [allowedSlippage, setAllowedSlippage] = useState(null); + + const form = useForm({ + reValidateMode: 'onChange', + mode: 'all', + defaultValues: { + assetIn: assetIds.assetIn, + assetOut: assetIds.assetOut, + }, + }); + const { + register, + handleSubmit, + watch, + getValues, + setValue, + trigger, + control, + formState, + } = form; + + const { isValid, isDirty, errors } = formState; + + const assetOutAmountInputRef = useRef(null); + const assetInAmountInputRef = useRef(null); + const shareAmountInputRef = useRef(null); + + // trigger form field validation right away + useEffect(() => { + trigger('submit'); + }, []); + + useEffect(() => { + // must provide input name otherwise it does not validate appropriately + trigger('submit'); + }, [ + isActiveAccountConnected, + pool, + isPoolLoading, + activeAccountTradeBalances, + assetInLiquidity, + assetOutLiquidity, + allowedSlippage, + ...watch(['assetInAmount', 'assetOutAmount']), + ]); + + // when the assetIds change, propagate the change to the parent + useEffect(() => { + const { assetIn, assetOut } = getValues(); + onAssetIdsChange({ assetIn, assetOut }); + }, watch(['assetIn', 'assetOut'])); + + const assetInAmountInput = useListenForInput(assetInAmountInputRef); + const assetOutAmountInput = useListenForInput(assetOutAmountInputRef); + const shareAssetAmountInput = useListenForInput(shareAmountInputRef); + + useEffect( + () => setValue('provisioningType', provisioningType), + [setValue, provisioningType] + ); + + const [lastAssetInteractedWith, setLastAssetInteractedWith] = useState< + string | null + >(); + + const calculateAssetIn = useCallback(() => { + setTimeout(() => { + const [assetOutAmount, shareAssetAmount, assetIn, assetOut] = getValues([ + 'assetOutAmount', + 'shareAssetAmount', + 'assetIn', + 'assetOut', + ]); + if ( + !pool || + !math || + !assetInLiquidity || + !assetOutLiquidity || + !activeAccountTradeBalances || + !assetIn || + !assetOut || + !shareAssetAmount + ) + return; + // if (provisioningType !== ProvisioningType.Add) return; + + // if (!assetOutAmount) return setValue('assetInAmount', null); + + // const amount = math.xyk.calculate_in_given_out( + // // which combination is correct? + // // assetOutLiquidity, + // // assetInLiquidity, + // assetInLiquidity, + // assetOutLiquidity, + // assetOutAmount + // ); + + if (provisioningType === ProvisioningType.Add && assetOutAmount) { + const amount = math.xyk.calculate_liquidity_in( + assetOutLiquidity, + assetInLiquidity, + assetOutAmount + ); + + console.log('calculateAssetIn2', { + assetOutLiquidity, assetInLiquidity, assetOutAmount, amount + }) + + // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in + if (amount === '0' && assetOutAmount !== '0') return; + setValue('assetInAmount', amount || null); + } else { + const amountA = math.xyk.calculate_liquidity_out_asset_a( + assetInLiquidity, + assetOutLiquidity, + shareAssetAmount, + pool.totalLiquidity + ); + + console.log('calculateAssetIn1', { + assetOutLiquidity, assetInLiquidity, assetOutAmount, amountA + }) + + // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in + // if (amountA === '0' && amountB !== '0') return; + setValue('assetInAmount', amountA || null); + } + }, 0); + }, [ + math, + getValues, + setValue, + pool, + assetInLiquidity, + assetOutLiquidity, + provisioningType, + activeAccountTradeBalances, + ]); + + const calculateAssetOut = useCallback(() => { + setTimeout(() => { + const [assetInAmount, shareAssetAmount, assetIn, assetOut] = getValues([ + 'assetInAmount', + 'shareAssetAmount', + 'assetIn', + 'assetOut', + ]); + + console.log('calculateAssetOut1', [assetInAmount, shareAssetAmount, assetIn, assetOut]); + + if ( + !pool || + !math || + !assetInLiquidity || + !assetOutLiquidity || + !activeAccountTradeBalances || + !assetIn || + !assetOut || + !shareAssetAmount + ) + return; + // if (provisioningType !== ProvisioningType.Remove) return; + + // if (!assetInAmount) return setValue('assetOutAmount', null); + + // const amount = math.xyk.calculate_out_given_in( + // assetInLiquidity, + // assetOutLiquidity, + // assetInAmount + // ); + // if (amount === '0' && assetInAmount !== '0') + // return setValue('assetOutAmount', null); + // setValue('assetOutAmount', amount || null); + + if (provisioningType === ProvisioningType.Add && assetInAmount) { + const amount = math.xyk.calculate_liquidity_in( + assetInLiquidity, + assetOutLiquidity, + assetInAmount + ); + + // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in + if (amount === '0' && assetInAmount !== '0' ) return; + setValue('assetOutAmount', amount || null); + } else { + const amountB = math.xyk.calculate_liquidity_out_asset_b( + assetInLiquidity, + assetOutLiquidity, + shareAssetAmount, + pool.totalLiquidity + ); + + + // do nothing deliberately, because the math library returns '0' as calculated value, as oppossed to calculate_out_given_in + // if (amountB === '0' && assetInAmount !== '0') return; + setValue('assetOutAmount', amountB || null); + } + }, 0); + }, [ + math, + getValues, + setValue, + pool, + assetInLiquidity, + assetOutLiquidity, + provisioningType, + activeAccountTradeBalances, + ]); + + useEffect(() => { + if (lastAssetInteractedWith === assetIds.assetIn) return; + calculateAssetIn(); + }, [ + calculateAssetIn, + lastAssetInteractedWith, + assetOutAmountInput, + assetIds + ]); + + useEffect(() => { + if (lastAssetInteractedWith === assetIds.assetOut) return; + calculateAssetOut(); + }, [ + calculateAssetOut, + lastAssetInteractedWith, + assetInAmountInput, + assetIds, + ]); + + useEffect(() => { + if (provisioningType === ProvisioningType.Remove) return; + const [assetInAmount, assetOutAmount, assetIn, assetOut] = getValues([ + 'assetInAmount', + 'assetOutAmount', + 'assetIn', + 'assetOut', + ]); + if (!assetIn || !assetOut || !assetInLiquidity || !assetInAmount || !pool) return; + + const shareAmount = math?.xyk.calculate_shares(assetInLiquidity, assetInAmount, pool?.totalLiquidity); + shareAmount && setValue('shareAssetAmount', shareAmount); + // assetIn > assetOut + // ? setValue('shareAssetAmount', assetOutAmount) + // : setValue('shareAssetAmount', assetInAmount); + }, [ + ...watch(['assetInAmount', 'assetOutAmount', 'assetIn', 'assetOut']), + math, + assetInLiquidity, + provisioningType, + getValues, + pool + ]); + + useEffect(() => { + setTimeout(() => { + if (provisioningType === ProvisioningType.Add) return; + const [ + assetInAmount, + assetOutAmount, + assetIn, + assetOut, + shareAssetAmount, + ] = getValues([ + 'assetInAmount', + 'assetOutAmount', + 'assetIn', + 'assetOut', + 'shareAssetAmount', + ]); + if (!assetIn || !assetOut) return; + console.log('calc', assetIn, assetOut) + calculateAssetIn(); + calculateAssetOut(); + }, 0); + }, [shareAssetAmountInput, calculateAssetIn, calculateAssetOut, provisioningType]); + + const getSubmitText = useCallback(() => { + if (isPoolLoading) return 'loading'; + + // TODO: change to 'input amounts'? + // if (!isDirty) return 'Swap'; + + switch (errors.submit?.type) { + case 'activeAccount': + return 'Select account'; + case 'poolDoesNotExist': + return 'Select tokens'; + } + + if (errors.assetInAmount || errors.assetOutAmount) return 'invalid amount'; + + return provisioningType === ProvisioningType.Add + ? 'Add Liquidity' + : 'Remove Liquidity'; + }, [isPoolLoading, errors, isDirty, provisioningType]); + + const modalContainerRef = useRef(null); + + const modalPortalElement = useModalPortalElement({ + allowedSlippage, + setAllowedSlippage, + }); + const { toggleModal, modalPortal, toggleId } = useModalPortal( + modalPortalElement, + modalContainerRef, + false + ); + + const tradeLimit = useMemo(() => { + // convert from precision, otherwise the math doesnt work + const assetInAmount = fromPrecision12( + getValues('assetInAmount') || undefined + ); + const assetOutAmount = fromPrecision12( + getValues('assetOutAmount') || undefined + ); + const assetIn = getValues('assetIn'); + const assetOut = getValues('assetOut'); + + if ( + !assetInAmount || + !assetOutAmount || + !spotPrice?.inOut || + !spotPrice?.outIn || + !assetIn || + !assetOut || + !allowedSlippage + ) + return; + + console.log('limit', { + assetInAmount, + spotPrice, + allowedSlippage + }) + + switch (lastAssetInteractedWith) { + case assetIds.assetIn: + return { + balance: new BigNumber(assetInAmount) + .multipliedBy(spotPrice?.inOut) + .multipliedBy(new BigNumber('1').plus(allowedSlippage)) + .toFixed(0), + assetId: assetOut, + }; + case assetIds.assetOut: + return { + balance: new BigNumber(assetOutAmount) + .multipliedBy(spotPrice?.outIn) + .multipliedBy(new BigNumber('1').plus(allowedSlippage)) + .toFixed(0), + assetId: assetIn, + }; + } + }, [ + spotPrice, + provisioningType, + allowedSlippage, + getValues, + assetIds, + lastAssetInteractedWith, + ...watch(['assetInAmount', 'assetOutAmount']), + ]); + + const slippage = useMemo(() => { + const assetInAmount = getValues('assetInAmount'); + const assetOutAmount = getValues('assetOutAmount'); + + if (!assetInAmount || !assetOutAmount || !spotPrice || !allowedSlippage) + return; + + switch (provisioningType) { + case ProvisioningType.Remove: + return percentageChange( + new BigNumber(assetInAmount).multipliedBy( + fromPrecision12(spotPrice.inOut) || '1' + ), + assetOutAmount + )?.abs(); + case ProvisioningType.Add: + return percentageChange( + new BigNumber(assetOutAmount).multipliedBy( + fromPrecision12(spotPrice.outIn) || '1' + ), + assetInAmount + )?.abs(); + } + }, [ + provisioningType, + getValues, + spotPrice, + ...watch(['assetInAmount', 'assetOutAmount']), + ]); + + useEffect(() => { + setLastAssetInteractedWith(assetIds.assetIn); + }, [assetInAmountInput, assetIds.assetIn]); + + useEffect(() => { + setLastAssetInteractedWith(assetIds.assetOut); + }, [assetOutAmountInput, assetIds.assetOut]); + + // handle submit of the form + const _handleSubmit = useCallback( + (data: PoolsFormFields) => { + if (!lastAssetInteractedWith) return; + onSubmit({ + ...data, + assetIn: lastAssetInteractedWith, + assetOut: + lastAssetInteractedWith === data.assetOut + ? data.assetIn + : data.assetOut, + assetInAmount: + lastAssetInteractedWith === data.assetOut + ? data.assetOutAmount + : data.assetInAmount, + assetOutAmount: + lastAssetInteractedWith === data.assetOut + ? data.assetInAmount + : data.assetOutAmount, + amountBMaxLimit: tradeLimit?.balance, + }); + }, + [ + provisioningType, + tradeLimit, + lastAssetInteractedWith, + assetIds, + tradeLimit, + ] + ); + + const handleSwitchAssets = useCallback( + (event: any) => { + onAssetIdsChange({ + assetIn: assetIds.assetOut, + assetOut: assetIds.assetIn, + }); + + // prevent form submit + event.preventDefault(); + if (lastAssetInteractedWith === assetIds.assetOut) { + setLastAssetInteractedWith(assetIds.assetIn) + const assetOutAmount = getValues('assetOutAmount'); + setValue('assetInAmount', assetOutAmount); + } else { + setLastAssetInteractedWith(assetIds.assetOut); + const assetInAmount = getValues('assetInAmount'); + setValue('assetOutAmount', assetInAmount); + } + + setTimeout(() => { + + }, 0); + }, + [assetIds, setValue, getValues, lastAssetInteractedWith] + ); + + const { apiInstance } = usePolkadotJsContext(); + const { cache } = useApolloClient(); + const [paymentInfo, setPaymentInfo] = useState(); + const { convertToFeePaymentAsset } = useMultiFeePaymentConversionContext(); + const calculatePaymentInfo = useCallback(async () => { + if (!apiInstance) return; + let [assetIn, assetOut, assetInAmount, assetOutAmount] = getValues([ + 'assetIn', + 'assetOut', + 'assetInAmount', + 'assetOutAmount', + ]); + + if ( + !assetIn || + !assetOut || + !assetInAmount || + !assetOutAmount || + !tradeLimit + ) + return; + + switch (provisioningType) { + case ProvisioningType.Add: { + const estimate = await estimateBuy( + cache, + apiInstance, + assetOut, + assetIn, + assetOutAmount, + tradeLimit.balance + ); + const partialFee = estimate?.partialFee.toString(); + return convertToFeePaymentAsset(partialFee); + } + case ProvisioningType.Remove: { + const estimate = await estimateSell( + cache, + apiInstance, + assetIn, + assetOut, + assetInAmount, + tradeLimit.balance + ); + const partialFee = estimate?.partialFee.toString(); + return convertToFeePaymentAsset(partialFee); + } + default: + return; + } + }, [ + apiInstance, + cache, + ...watch(['assetInAmount', 'assetOutAmount', 'assetIn']), + tradeLimit, + provisioningType, + convertToFeePaymentAsset, + ]); + + useEffect(() => { + (async () => { + const paymentInfo = await calculatePaymentInfo(); + if (!paymentInfo) return; + setPaymentInfo(paymentInfo); + })(); + }, [ + apiInstance, + cache, + ...watch(['assetInAmount', 'assetOutAmount']), + tradeLimit, + provisioningType, + calculatePaymentInfo + ]); + + useEffect(() => { + setValue('assetIn', assetIds.assetIn); + setValue('assetOut', assetIds.assetOut); + }, [assetIds]); + + const tradeBalances = useMemo(() => { + const assetOutAmount = getValues('assetOutAmount'); + const outBeforeTrade = activeAccountTradeBalances?.outBalance?.balance; + const outAfterTrade = + (outBeforeTrade && + assetOutAmount && + new BigNumber(outBeforeTrade).plus(assetOutAmount).toFixed(0)) || + undefined; + const outTradeChange = + outBeforeTrade !== '0' + ? percentageChange( + fromPrecision12(outBeforeTrade), + fromPrecision12(outAfterTrade) + )?.multipliedBy(100) + : new BigNumber( + outAfterTrade && outAfterTrade !== '0' ? '100.000' : '0' + ); + + const assetInAmount = getValues('assetInAmount'); + const inBeforeTrade = activeAccountTradeBalances?.inBalance?.balance; + let inAfterTrade = + (inBeforeTrade && + assetInAmount && + new BigNumber(inBeforeTrade).minus(assetInAmount).toFixed(0)) || + undefined; + + inAfterTrade = + getValues('assetIn') !== '0' + ? inAfterTrade + : paymentInfo && + inAfterTrade && + new BigNumber(inAfterTrade).minus(paymentInfo).toFixed(0); + + const inTradeChange = + inBeforeTrade !== '0' + ? percentageChange( + fromPrecision12(inBeforeTrade), + fromPrecision12(inAfterTrade) + )?.multipliedBy(100) + : new BigNumber( + inAfterTrade && inAfterTrade !== '0' ? '-100.000' : '0' + ); + + return { + outBeforeTrade, + outAfterTrade, + outTradeChange, + + inBeforeTrade, + inAfterTrade, + inTradeChange, + }; + }, [ + activeAccountTradeBalances, + ...watch(['assetOutAmount', 'assetInAmount', 'assetIn']), + paymentInfo, + ]); + + const { debugComponent } = useDebugBoxContext(); + + useEffect(() => { + debugComponent('PoolsForm', { + ...getValues(), + spotPrice, + tradeLimit, + assetInLiquidity, + assetOutLiquidity, + tradeBalances: { + ...tradeBalances, + inTradeChange: tradeBalances.inTradeChange?.toString(), + outTradeChange: tradeBalances.outTradeChange?.toString(), + }, + provisioningType, + slippage: slippage?.toString(), + errors: Object.keys(errors).reduce((reducedErrors, error) => { + return { + ...reducedErrors, + [error]: (errors as any)[error].type, + }; + }, {}), + }); + }, [ + Object.values(getValues()).toString(), + spotPrice, + tradeBalances, + tradeBalances, + provisioningType, + errors, + assetInLiquidity, + assetOutLiquidity, + slippage, + formState.isDirty, + ]); + + const minTradeLimitIn = useCallback( + (assetInAmount?: Maybe) => { + if (!assetInAmount || assetInAmount === '0') return false; + return new BigNumber(assetInLiquidity || '0') + .dividedBy(3) + .gte(assetInAmount); + }, + [assetInLiquidity] + ); + + const [maxAmountInLoading, setMaxAmountInLoading] = useState(false); + + const calculateMaxAmountIn = useCallback(async () => { + const [assetIn, assetOut] = getValues(['assetIn', 'assetOut']); + console.log( + 'calculateMaxAmountIn1', + tradeBalances.inBeforeTrade, + cache, + apiInstance, + assetIn, + assetOut + ); + if ( + !tradeBalances.inBeforeTrade || + !cache || + !apiInstance || + !assetIn || + !assetOut + ) + return; + console.log('calculateMaxAmountIn11'); + const maxAmount = tradeBalances.inBeforeTrade; + const estimate = await estimateSell( + cache, + apiInstance, + assetIn, + assetOut, + maxAmount, + '0' + ); + console.log('calculateMaxAmountIn11 estimate done', estimate); + const paymentInfo = estimate?.partialFee.toString(); + const maxAmountWithoutFee = new BigNumber(maxAmount).minus( + paymentInfo || '0' + ); + console.log('calculateMaxAmountIn12', { + inBeforeTrade: tradeBalances.inBeforeTrade, + estimate, + paymentInfo, + maxAmount, + maxAmountWithoutFee: maxAmountWithoutFee.toFixed(10), + }); + + return getValues('assetIn') === '0' + ? // max amount changed when all fields are filled out since that allows + // us to calculate paymentInfo + maxAmountWithoutFee.gt('0') + ? maxAmountWithoutFee.toFixed(10) + : undefined + : maxAmount; + }, [ + tradeBalances.inBeforeTrade, + paymentInfo, + cache, + apiInstance, + ...watch(['assetIn']), + ]); + + const maxButtonDisabled = useMemo(() => { + return ( + maxAmountInLoading || activeAccountTradeBalancesLoading || isPoolLoading + ); + }, [maxAmountInLoading, activeAccountTradeBalancesLoading, isPoolLoading]); + + const handleMaxButtonOnClick = useCallback(async () => { + setMaxAmountInLoading(true); + const maxAmountIn = await calculateMaxAmountIn(); + console.log('setting max amount in', maxAmountIn); + if (maxAmountIn) + setValue('assetInAmount', maxAmountIn, { + shouldDirty: true, + shouldValidate: true, + }); + setMaxAmountInLoading(false); + }, [calculateMaxAmountIn]); + + return ( +
+
+ {modalPortal} + + +
+
+
+ + +
+
{ + e.preventDefault(); + toggleModal(); + }} + > + +
+
+ +
+ {provisioningType === ProvisioningType.Add ? 'Add' : 'Remove'}{' '} + Liquidity +
+
+ !Object.values(assetIds).includes(asset.id) + )} + disabled={provisioningType === ProvisioningType.Remove} + maxBalanceLoading={maxAmountInLoading} + /> +
+
First token
+ {activeAccountTradeBalancesLoading || isPoolLoading ? ( + 'Your balance: loading' + ) : ( + <> + Your balance: + {assetIds.assetIn ? ( + tradeBalances.inBeforeTrade !== undefined ? ( + + ) : ( + <> {horizontalBar} + ) + ) : ( + <> {horizontalBar} + )} + + )} + {/*
handleMaxButtonOnClick()} + > + Max +
*/} +
+
+ +
+
+ {/*
+ +
*/} +
+
+ {(() => { + const assetOut = getValues('assetOut'); + const assetIn = getValues('assetIn'); + switch (provisioningType) { + case ProvisioningType.Remove: + // return `1 ${ + // idToAsset(getValues('assetIn'))?.symbol || + // getValues('assetIn') + // } = ${fromPrecision12(spotPrice?.inOut)} ${ + // idToAsset(getValues('assetOut'))?.symbol || + // getValues('assetOut') + // }`; + return spotPrice?.inOut && assetOut ? ( + <> + + = + + + ) : ( + <>- + ); + case ProvisioningType.Add: + // return `1 ${ + // idToAsset(getValues('assetOut'))?.symbol || + // getValues('assetOut') + // } = ${fromPrecision12(spotPrice?.outIn)} ${ + // idToAsset(getValues('assetIn'))?.symbol || + // getValues('assetIn') + // }`; + return spotPrice?.outIn && assetIn ? ( + <> + + = + + + ) : ( + <>- + ); + } + })()} +
+
+
+ +
+ {' '} + !Object.values(assetIds).includes(asset.id) + )} + disabled={provisioningType === ProvisioningType.Remove} + />{' '} +
+
Second token
+ {activeAccountTradeBalancesLoading || isPoolLoading ? ( + 'Your balance: loading' + ) : ( + // : `${fromPrecision12(tradeBalances.outBeforeTrade)} -> ${fromPrecision12(tradeBalances.outAfterTrade)}` + <> + Your balance: + {assetIds.assetOut ? ( + tradeBalances.outBeforeTrade !== undefined ? ( + + ) : ( + <> {horizontalBar} + ) + ) : ( + <> {horizontalBar} + )} + + )} +
+
+
+ {' '} + !Object.values(assetIds).includes(asset.id) + )} + disabled={provisioningType === ProvisioningType.Add} + />{' '} +
+
Share token
+ {activeAccountTradeBalancesLoading || isPoolLoading ? ( + 'Your balance: loading' + ) : ( + // : `${fromPrecision12(tradeBalances.outBeforeTrade)} -> ${fromPrecision12(tradeBalances.outAfterTrade)}` + <> + Your balance: + {activeAccountTradeBalances?.shareBalance ? ( + + ) : ( + <> {horizontalBar} + )} + + )} +
+
+ + + isActiveAccountConnected, + poolDoesNotExist: () => !isPoolLoading && !!pool, + notEnoughBalanceInA: () => { + if (provisioningType === ProvisioningType.Remove) return true; + const assetInAmount = getValues('assetInAmount'); + if ( + !activeAccountTradeBalances?.inBalance?.balance || + !assetInAmount + ) + return false; + return new BigNumber( + activeAccountTradeBalances.inBalance.balance + ).gte(assetInAmount); + }, + notEnoughBalanceInB: () => { + if (provisioningType === ProvisioningType.Remove) return true; + const assetInAmount = getValues('assetOutAmount'); + if ( + !activeAccountTradeBalances?.outBalance?.balance || + !assetInAmount + ) + return false; + return new BigNumber( + activeAccountTradeBalances.outBalance.balance + ).gte(assetInAmount); + }, + notEnoughBalanceInShare: () => { + if (provisioningType === ProvisioningType.Add) return true; + const shareAssetAmount = getValues('shareAssetAmount'); + if ( + !activeAccountTradeBalances?.shareBalance?.balance || + !shareAssetAmount + ) + return false; + return new BigNumber( + activeAccountTradeBalances.shareBalance.balance + ).gte(shareAssetAmount); + }, + // notEnoughFeeBalance: () => { + // const assetIn = getValues('assetIn'); + // const assetInAmount = getValues('assetInAmount'); + + // let nativeAssetBalance = find(activeAccount?.balances, { + // assetId: '0', + // })?.balance; + + // let balanceForFee = nativeAssetBalance; + + // if (assetIn === '0' && assetInAmount && nativeAssetBalance) { + // balanceForFee = new BigNumber(nativeAssetBalance) + // .minus(assetInAmount) + // .toString(); + // } + + // if (!paymentInfo) return true; + // if (!balanceForFee) return false; + + // return new BigNumber(balanceForFee).gte(paymentInfo); + // }, + }, + })} + disabled={!isValid || tradeLoading || !isDirty} + value={getSubmitText()} + /> + +
+
+ ); +}; diff --git a/src/components/Pools/PoolsInfo/PoolsInfo.scss b/src/components/Pools/PoolsInfo/PoolsInfo.scss new file mode 100644 index 00000000..986bf06a --- /dev/null +++ b/src/components/Pools/PoolsInfo/PoolsInfo.scss @@ -0,0 +1,77 @@ +@import './../../../misc/colors.module.scss'; +@import './../../../misc/misc.module.scss'; + +.pools-info { + display: flex; + flex-direction: column; + justify-content: start; + gap: 4px; + + font-size: 15px; + font-weight: 400; + margin-top: 4px; + margin-bottom: 4px; + + &__data { + display: flex; + flex-direction: column; + + justify-content: center; + + max-height: 120px; + opacity: 1; + + transition: max-height 0.3s ease, opacity 0.15s ease; + + &.hidden { + max-height: 0px; + opacity: 0; + } + + .data-piece { + padding: 2px 0 4px 0; + display: flex; + justify-content: space-between; + align-items: center; + &__label { + color: #9ea9b1; + } + position: relative; + + &:not(:last-child):after { + content: ' '; + position: absolute; + width: 100%; + height: 1px; + background-color: #26282f; + bottom: 0; + } + } + } + + .validation { + opacity: 0; + line-height: 16px; + padding: 0 16px; + height: 100%; + max-height: 0px; + overflow: hidden; + + transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease; + border-radius: 8px; + + &.visible { + max-height: 80px; + padding: 16px; + opacity: 1; + } + + &.error { + background: rgba(255, 104, 104, 0.3); + } + + &.warning { + color: $orange1; + } + } +} diff --git a/src/components/Pools/PoolsInfo/PoolsInfo.tsx b/src/components/Pools/PoolsInfo/PoolsInfo.tsx new file mode 100644 index 00000000..71b6fa49 --- /dev/null +++ b/src/components/Pools/PoolsInfo/PoolsInfo.tsx @@ -0,0 +1,128 @@ +import BigNumber from 'bignumber.js'; +import { debounce, delay, throttle } from 'lodash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { FieldErrors } from 'react-hook-form'; +import { useMultiFeePaymentConversionContext } from '../../../containers/MultiProvider'; +import { Balance, Fee } from '../../../generated/graphql'; +import { FormattedBalance } from '../../Balance/FormattedBalance/FormattedBalance'; +import { horizontalBar } from '../../Chart/ChartHeader/ChartHeader'; +import { PoolsFormFields, ProvisioningType } from '../PoolsForm'; +import constants from '../../../constants'; +import './PoolsInfo.scss'; + +export interface PoolsInfoProps { + transactionFee?: string; + tradeLimit?: Balance; + isDirty?: boolean; + errors?: FieldErrors; + paymentInfo?: string; + provisioningType: ProvisioningType; +} + +export const PoolsInfo = ({ + errors, + tradeLimit, + provisioningType, + isDirty, + paymentInfo, +}: PoolsInfoProps) => { + const [displayError, setDisplayError] = useState(); + const isError = useMemo(() => !!errors?.submit?.type, [errors?.submit]); + const formError = useMemo(() => { + switch (errors?.submit?.type) { + case 'slippageHigherThanTolerance': + return 'Slippage higher than tolerance'; + case 'notEnoughBalanceInA': + return 'Insufficient Token A balance'; + case 'notEnoughBalanceInB': + return 'Insufficient Token B balance'; + case 'notEnoughBalanceInShare': + return 'Insufficient Share token balance'; + case 'notEnoughFeeBalance': + return 'Insufficient fee balance'; + case 'poolDoesNotExist': + return 'Please select valid pool'; + case 'activeAccount': + return 'Please connect a wallet to continue'; + } + return; + }, [errors?.submit]); + + useEffect(() => { + if (formError) { + const timeoutId = setTimeout(() => setDisplayError(formError), 50); + return () => timeoutId && clearTimeout(timeoutId); + } + const timeoutId = setTimeout(() => setDisplayError(formError), 300); + return () => timeoutId && clearTimeout(timeoutId); + }, [formError]); + + const { feePaymentAsset } = useMultiFeePaymentConversionContext(); + + return ( +
+
+ {/*
+ Current slippage +
+ {!expectedSlippage || expectedSlippage?.isNaN() + ? horizontalBar + : `${expectedSlippage?.multipliedBy(100).toFixed(2)}%`} +
+
*/} + {provisioningType === ProvisioningType.Add + ? ( +
+ Provisioning limit +
+ {tradeLimit?.balance ? ( + + ) : ( + <>{horizontalBar} + )} +
+
+ ) + : <> + } +
+ Transaction fee +
+ {paymentInfo ? ( + + ) : ( + <>{horizontalBar} + )} +
+
+ {/*
+ Trade fee +
+ {new BigNumber(tradeFee.numerator) + .dividedBy(tradeFee.denominator) + .multipliedBy(100) + .toFixed(2)} + % +
+
*/} +
+ {/* TODO Error message */} + +
+ {displayError} +
+
+ ); +}; diff --git a/src/components/Trade/TradeForm/TradeForm.scss b/src/components/Trade/TradeForm/TradeForm.scss index 591cb865..055a26e7 100644 --- a/src/components/Trade/TradeForm/TradeForm.scss +++ b/src/components/Trade/TradeForm/TradeForm.scss @@ -6,52 +6,83 @@ flex-basis: 350px; flex-grow: 1; - padding: 14px; + padding: 22px; min-width: 350px; + max-width: 610px; + margin: 0 auto; - background-color: $d-gray4; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05); + background: linear-gradient(180deg, #1c2527 0%, #14161a 80.73%, #121316 100%); overflow: hidden; + border-radius: 10px; position: relative; + color: white; + .trade-form { display: flex; flex-direction: column; justify-content: space-between; - gap: 8px; + gap: 14px; height: 100%; min-height: 400px; .trade-form-heading { + width: fit-content; padding-top: 4px; color: $l-gray3; - font-size: 18px; + font-size: 22px; font-weight: 500; + background: linear-gradient( + 90deg, + #4fffb0 1.27%, + #b3ff8f 48.96%, + #ff984e 104.14% + ), + linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%), + linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; } .divider-wrapper { display: flex; align-items: center; - height: 2px; + height: 1px; width: 100%; } .divider { position: absolute; width: 100%; - height: 2px; - background-color: $d-gray5; + height: 1px; + background-color: rgba(76, 243, 168, 0.12); opacity: 1; border: 0; left: 0; } + .balance-wrapper { + display: flex; + flex-direction: column-reverse; + align-items: end; + background: rgba(162, 176, 187, 0.1); + padding: 12px; + padding-top: 18px; + border-radius: 10px; + gap: 6px; + padding-bottom: 16px; + } + .submit-button { background: $green1; text-transform: uppercase; - border-radius: $border-radius; + border-radius: 36px; height: 50px; color: $d-gray4; @@ -72,37 +103,44 @@ .balance-info { display: flex; align-items: center; + justify-content: right; + width: 100%; gap: 4px; height: 16px; margin-top: 4px; - font-size: 10px; - line-height: 10px; + font-size: 12px; + line-height: 12px; + position: relative; + + .balance-info-type { + position: absolute; + left: 0; + font-weight: 600; + font-size: 16px; + color: $green1; + padding: 6px; + } } .asset-switch { display: flex; - height: 55px; + height: 43px; justify-content: space-between; align-items: center; width: 100%; .asset-switch-icon { position: absolute; - left: 16px; + left: 24px; display: flex; align-items: center; justify-content: center; - width: 55px; - height: 55px; - - border-radius: 55px; - border: 4px solid $d-gray3; - background-color: $d-gray5; - overflow: hidden; + background: #192022; + border-radius: 50%; transition: transform 500ms ease; @@ -120,26 +158,37 @@ } .asset-switch-price { - display: flex; - align-items: center; - gap: 4px; - height: 18px; - - right: 0; - padding: 0 16px; - font-size: 12px; - font-weight: 500; position: absolute; + right: 24px; + background: #192022; - background-color: $d-gray5; + &__wrapper { + display: flex; + align-items: center; + gap: 4px; - border-radius: 4px 0 0 4px; + padding: 4px 14px; + font-size: 11px; + font-weight: 500; + + background: rgba(218, 255, 238, 0.06); + border-radius: 7px; + } } } .settings-button { position: absolute; - right: 16px; + display: flex; + right: 24px; + top: 20px; + padding: 10px 8px; + border-radius: 50%; + background-color: rgba(162, 176, 187, 0.1); + + svg { + width: 24px; + } &:hover { cursor: pointer; @@ -160,12 +209,25 @@ left: 0; z-index: 1; + + .disclaimer { + padding: 12px 24px; + padding-top: 0px; + font-size: 14px; + color: $gray4; + } .trade-settings { height: 100%; } + .settings-section { + padding: 12px 24px; + background: linear-gradient(0deg, #171518, #171518), #1c1a1f; + } + .settings-field { + padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; @@ -201,3 +263,19 @@ background-color: rgba(0, 0, 0, 0.8); } + +.max-button { + font-size: 12px; + font-weight: 400; + color: $white1; + padding: 4px 10px; + background: rgba(255, 255, 255, 0.06); + border-radius: 12px; + text-transform: capitalize; + cursor: pointer; + + &.disabled { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/src/components/Trade/TradeForm/TradeForm.tsx b/src/components/Trade/TradeForm/TradeForm.tsx index 02098da7..c4e32e6a 100644 --- a/src/components/Trade/TradeForm/TradeForm.tsx +++ b/src/components/Trade/TradeForm/TradeForm.tsx @@ -11,7 +11,13 @@ import { useState, } from 'react'; import { Control, FormProvider, useForm } from 'react-hook-form'; -import { Account, Balance, Maybe, Pool, TradeType } from '../../../generated/graphql'; +import { + Account, + Balance, + Maybe, + Pool, + TradeType, +} from '../../../generated/graphql'; import { fromPrecision12 } from '../../../hooks/math/useFromPrecision'; import { useMath } from '../../../hooks/math/useMath'; import { percentageChange } from '../../../hooks/math/usePercentageChange'; @@ -31,6 +37,8 @@ import { usePolkadotJsContext } from '../../../hooks/polkadotJs/usePolkadotJs'; import { useApolloClient } from '@apollo/client'; import { estimateBuy } from '../../../hooks/pools/xyk/buy'; import { estimateSell } from '../../../hooks/pools/xyk/sell'; +import { payment } from '@polkadot/types/interfaces/definitions'; +import { useMultiFeePaymentConversionContext } from '../../../containers/MultiProvider'; export interface TradeFormSettingsProps { allowedSlippage: string | null; @@ -48,13 +56,14 @@ export const TradeFormSettings = ({ onAllowedSlippageChange, closeModal, }: TradeFormSettingsProps) => { - const { register, watch, getValues, setValue, handleSubmit } = - useForm({ - defaultValues: { - allowedSlippage, - autoSlippage: true, - }, - }); + const { register, watch, getValues, setValue, handleSubmit } = useForm< + TradeFormSettingsFormFields + >({ + defaultValues: { + allowedSlippage, + autoSlippage: true, + }, + }); // propagate allowed slippage to the parent useEffect(() => { @@ -71,32 +80,38 @@ export const TradeFormSettings = ({ return (
{})} >
- Settings +
Settings
- +
+
Trade settings
+
+ The deviation of the final acceptable price from the spot price caused by protocol fee, price impact (depends on trade & pool size) and change in price between announcing the transaction and processing it. +
); @@ -149,7 +164,7 @@ export interface TradeFormProps { inBalance?: Balance; }; activeAccountTradeBalancesLoading: boolean; - activeAccount?: Maybe + activeAccount?: Maybe; } export interface TradeFormFields { @@ -202,7 +217,7 @@ export const TradeForm = ({ assets, activeAccountTradeBalances, activeAccountTradeBalancesLoading, - activeAccount + activeAccount, }: TradeFormProps) => { // TODO: include math into loading form state const { math, loading: mathLoading } = useMath(); @@ -238,6 +253,20 @@ export const TradeForm = ({ trigger('submit'); }, []); + useEffect(() => { + // must provide input name otherwise it does not validate appropriately + trigger('submit'); + }, [ + isActiveAccountConnected, + pool, + isPoolLoading, + activeAccountTradeBalances, + assetInLiquidity, + assetOutLiquidity, + allowedSlippage, + ...watch(['assetInAmount', 'assetOutAmount']), + ]); + // when the assetIds change, propagate the change to the parent useEffect(() => { const { assetIn, assetOut } = getValues(); @@ -330,8 +359,12 @@ export const TradeForm = ({ const tradeLimit = useMemo(() => { // convert from precision, otherwise the math doesnt work - const assetInAmount = fromPrecision12(getValues('assetInAmount') || undefined); - const assetOutAmount = fromPrecision12(getValues('assetOutAmount') || undefined); + const assetInAmount = fromPrecision12( + getValues('assetInAmount') || undefined + ); + const assetOutAmount = fromPrecision12( + getValues('assetOutAmount') || undefined + ); const assetIn = getValues('assetIn'); const assetOut = getValues('assetOut'); @@ -436,37 +469,98 @@ export const TradeForm = ({ assetIn: assetIds.assetOut, assetOut: assetIds.assetIn, }); + + if (tradeType === TradeType.Buy) { + const assetOutAmount = getValues('assetOutAmount'); + setValue('assetInAmount', assetOutAmount); + setTradeType(TradeType.Sell); + setValue('assetOutAmount', null) + } else { + const assetInAmount = getValues('assetInAmount'); + setValue('assetOutAmount', assetInAmount); + setTradeType(TradeType.Buy); + setValue('assetInAmount', null) + } }, - [assetIds] + [assetIds, tradeType, setValue, getValues, setTradeType] ); - const { apiInstance } = usePolkadotJsContext() + const { apiInstance } = usePolkadotJsContext(); const { cache } = useApolloClient(); const [paymentInfo, setPaymentInfo] = useState(); - useEffect(() => { + const { convertToFeePaymentAsset, feePaymentAsset } = useMultiFeePaymentConversionContext(); + const calculatePaymentInfo = useCallback(async () => { if (!apiInstance) return; - const [ assetIn, assetOut, assetInAmount, assetOutAmount ] = getValues(['assetIn', 'assetOut', 'assetInAmount', 'assetOutAmount']); - - if (!assetIn || !assetOut || !assetInAmount || !assetOutAmount || !tradeLimit) return; + let [assetIn, assetOut, assetInAmount, assetOutAmount] = getValues([ + 'assetIn', + 'assetOut', + 'assetInAmount', + 'assetOutAmount', + ]); - (async () => { - switch (tradeType) { - case TradeType.Buy: { - const estimate = (await estimateBuy(cache, apiInstance, assetOut, assetIn, assetOutAmount, tradeLimit.balance)) - const partialFee = estimate?.partialFee.toString(); - return setPaymentInfo(partialFee); - } - case TradeType.Sell: { - const estimate = (await estimateSell(cache, apiInstance, assetIn, assetOut, assetInAmount, tradeLimit.balance)) - const partialFee = estimate?.partialFee.toString(); - return setPaymentInfo(partialFee); - } - default: - return; + if ( + !assetIn || + !assetOut || + !assetInAmount || + !assetOutAmount || + !tradeLimit + ) + return; + + switch (tradeType) { + case TradeType.Buy: { + const estimate = await estimateBuy( + cache, + apiInstance, + assetOut, + assetIn, + assetOutAmount, + tradeLimit.balance + ); + const partialFee = estimate?.partialFee.toString(); + return convertToFeePaymentAsset(partialFee); } + case TradeType.Sell: { + const estimate = await estimateSell( + cache, + apiInstance, + assetIn, + assetOut, + assetInAmount, + tradeLimit.balance + ); + const partialFee = estimate?.partialFee.toString(); + return convertToFeePaymentAsset(partialFee); + } + default: + return; + } + }, [ + apiInstance, + cache, + ...watch(['assetInAmount', 'assetOutAmount', 'assetIn']), + tradeLimit, + tradeType, + convertToFeePaymentAsset, + feePaymentAsset, + getValues, + pool + ]); + + useEffect(() => { + (async () => { + const paymentInfo = await calculatePaymentInfo(); + if (!paymentInfo) return; + setPaymentInfo(paymentInfo); })(); - - }, [apiInstance, cache, ...watch(['assetInAmount', 'assetOutAmount']), tradeLimit, tradeType]); + }, [ + apiInstance, + cache, + ...watch(['assetInAmount', 'assetOutAmount']), + tradeLimit, + tradeType, + calculatePaymentInfo + ]); useEffect(() => { setValue('assetIn', assetIds.assetIn); @@ -477,30 +571,44 @@ export const TradeForm = ({ const assetOutAmount = getValues('assetOutAmount'); const outBeforeTrade = activeAccountTradeBalances?.outBalance?.balance; const outAfterTrade = - outBeforeTrade && - assetOutAmount && - new BigNumber(outBeforeTrade).plus(assetOutAmount).toFixed(0) || undefined; + (outBeforeTrade && + assetOutAmount && + new BigNumber(outBeforeTrade).plus(assetOutAmount).toFixed(0)) || + undefined; const outTradeChange = outBeforeTrade !== '0' ? percentageChange( fromPrecision12(outBeforeTrade), fromPrecision12(outAfterTrade) )?.multipliedBy(100) - : new BigNumber(outAfterTrade && outAfterTrade !== '0' ? '100.000' : '0'); + : new BigNumber( + outAfterTrade && outAfterTrade !== '0' ? '100.000' : '0' + ); const assetInAmount = getValues('assetInAmount'); const inBeforeTrade = activeAccountTradeBalances?.inBalance?.balance; - const inAfterTrade = - inBeforeTrade && - assetInAmount && - new BigNumber(inBeforeTrade).minus(assetInAmount).toFixed(0) || undefined + let inAfterTrade = + (inBeforeTrade && + assetInAmount && + new BigNumber(inBeforeTrade).minus(assetInAmount).toFixed(0)) || + undefined; + + inAfterTrade = + getValues('assetIn') !== '0' + ? inAfterTrade + : paymentInfo && + inAfterTrade && + new BigNumber(inAfterTrade).minus(paymentInfo).toFixed(0); + const inTradeChange = inBeforeTrade !== '0' ? percentageChange( fromPrecision12(inBeforeTrade), fromPrecision12(inAfterTrade) )?.multipliedBy(100) - : new BigNumber(inAfterTrade && inAfterTrade !== '0' ? '-100.000' : '0'); + : new BigNumber( + inAfterTrade && inAfterTrade !== '0' ? '-100.000' : '0' + ); return { outBeforeTrade, @@ -513,13 +621,13 @@ export const TradeForm = ({ }; }, [ activeAccountTradeBalances, - ...watch(['assetOutAmount', 'assetInAmount']), + ...watch(['assetOutAmount', 'assetInAmount', 'assetIn']), + paymentInfo, ]); const { debugComponent } = useDebugBoxContext(); useEffect(() => { - console.log('all values', getValues()); debugComponent('TradeForm', { ...getValues(), spotPrice, @@ -549,24 +657,102 @@ export const TradeForm = ({ errors, assetInLiquidity, assetOutLiquidity, - slippage + slippage, + formState.isDirty, ]); - useEffect(() => { - // must provide input name otherwise it does not validate appropriately - trigger('submit'); + const minTradeLimitIn = useCallback( + (assetInAmount?: Maybe) => { + if (!assetInAmount || assetInAmount === '0') return false; + return new BigNumber(assetInLiquidity || '0') + .dividedBy(3) + .gte(assetInAmount); + }, + [assetInLiquidity] + ); + + const [maxAmountInLoading, setMaxAmountInLoading] = useState(false); + + const calculateMaxAmountIn = useCallback(async () => { + const [assetIn, assetOut] = getValues(['assetIn', 'assetOut']); + console.log( + 'calculateMaxAmountIn1', + tradeBalances.inBeforeTrade, + cache, + apiInstance, + assetIn, + assetOut + ); + if ( + !tradeBalances.inBeforeTrade || + !cache || + !apiInstance || + !assetIn || + !assetOut + ) + return; + console.log('calculateMaxAmountIn11'); + const maxAmount = tradeBalances.inBeforeTrade; + const estimate = await estimateSell( + cache, + apiInstance, + assetIn, + assetOut, + maxAmount, + '0' + ); + console.log('calculateMaxAmountIn11 estimate done', estimate); + const paymentInfo = estimate?.partialFee.toString(); + const maxAmountWithoutFee = new BigNumber(maxAmount).minus( + (feePaymentAsset === getValues('assetIn') + ? feePaymentAsset === '0' + ? paymentInfo + : convertToFeePaymentAsset(paymentInfo) + : '0' + ) || '0' + ); + console.log('calculateMaxAmountIn12', { + inBeforeTrade: tradeBalances.inBeforeTrade, + estimate, + paymentInfo, + maxAmount, + maxAmountWithoutFee: maxAmountWithoutFee.toFixed(10), + }); + + return getValues('assetIn') === feePaymentAsset + ? // max amount changed when all fields are filled out since that allows + // us to calculate paymentInfo + maxAmountWithoutFee.gt('0') + ? maxAmountWithoutFee.toFixed(10) + : undefined + : maxAmount; }, [ - isActiveAccountConnected, - pool, - isPoolLoading, - activeAccountTradeBalances, - assetInLiquidity, - assetOutLiquidity, - allowedSlippage, + tradeBalances.inBeforeTrade, paymentInfo, - ...watch(['assetInAmount', 'assetOutAmount']), + cache, + apiInstance, + feePaymentAsset, convertToFeePaymentAsset, + ...watch(['assetIn']), ]); + const maxButtonDisabled = useMemo(() => { + return ( + maxAmountInLoading || activeAccountTradeBalancesLoading || isPoolLoading + ); + }, [maxAmountInLoading, activeAccountTradeBalancesLoading, isPoolLoading]); + + const handleMaxButtonOnClick = useCallback(async () => { + setMaxAmountInLoading(true); + const maxAmountIn = await calculateMaxAmountIn(); + console.log('setting max amount in', maxAmountIn); + if (maxAmountIn) + setValue('assetInAmount', maxAmountIn, { + shouldDirty: true, + shouldValidate: true, + }); + setMaxAmountInLoading(false); + }, [calculateMaxAmountIn]); + return (
@@ -584,65 +770,49 @@ export const TradeForm = ({
-
Pay with
+
Trade Tokens
!Object.values(assetIds).includes(asset.id))} + assets={assets?.filter( + (asset) => !Object.values(assetIds).includes(asset.id) + )} + maxBalanceLoading={maxAmountInLoading} />
- {activeAccountTradeBalancesLoading || - isPoolLoading - ? ( +
Pay with
+ {activeAccountTradeBalancesLoading || isPoolLoading ? ( 'Your balance: loading' ) : ( - // : `${fromPrecision12(tradeBalances.outBeforeTrade)} -> ${fromPrecision12(tradeBalances.outAfterTrade)}` <> Your balance: {assetIds.assetIn ? ( - tradeBalances.inBeforeTrade !== undefined - ? ( - - ) - : <> {horizontalBar} - ) : ( - <> {horizontalBar} - )} - {tradeBalances.inAfterTrade !== undefined && tradeBalances.inBeforeTrade !== undefined && assetIds.assetIn ? ( - <> - + tradeBalances.inBeforeTrade !== undefined ? ( - + ) : ( + <> {horizontalBar} + ) ) : ( - <> + <> {horizontalBar} )} - {tradeBalances.inTradeChange && - !tradeBalances.inTradeChange.isZero() && ( -
- ( - {tradeBalances.inTradeChange?.abs().lt('0.01') - ? `< -0.01` - : tradeBalances.inTradeChange?.abs().gt('1000') - ? `> -1000` - : tradeBalances.inTradeChange.toFixed(2)} - %) -
- )} )} +
handleMaxButtonOnClick()} + > + Max +
@@ -652,70 +822,71 @@ export const TradeForm = ({
- {(() => { - const assetOut = getValues('assetOut'); - const assetIn = getValues('assetIn'); - switch (tradeType) { - case TradeType.Sell: - // return `1 ${ - // idToAsset(getValues('assetIn'))?.symbol || - // getValues('assetIn') - // } = ${fromPrecision12(spotPrice?.inOut)} ${ - // idToAsset(getValues('assetOut'))?.symbol || - // getValues('assetOut') - // }`; - return spotPrice?.inOut && assetOut ? ( - <> - - = - - - ) : ( - <>- - ); - case TradeType.Buy: - // return `1 ${ - // idToAsset(getValues('assetOut'))?.symbol || - // getValues('assetOut') - // } = ${fromPrecision12(spotPrice?.outIn)} ${ - // idToAsset(getValues('assetIn'))?.symbol || - // getValues('assetIn') - // }`; - return spotPrice?.outIn && assetIn ? ( - <> - - = - - - ) : ( - <>- - ); - } - })()} +
+ {(() => { + const assetOut = getValues('assetOut'); + const assetIn = getValues('assetIn'); + switch (tradeType) { + case TradeType.Sell: + // return `1 ${ + // idToAsset(getValues('assetIn'))?.symbol || + // getValues('assetIn') + // } = ${fromPrecision12(spotPrice?.inOut)} ${ + // idToAsset(getValues('assetOut'))?.symbol || + // getValues('assetOut') + // }`; + return spotPrice?.inOut && assetOut ? ( + <> + + = + + + ) : ( + <>- + ); + case TradeType.Buy: + // return `1 ${ + // idToAsset(getValues('assetOut'))?.symbol || + // getValues('assetOut') + // } = ${fromPrecision12(spotPrice?.outIn)} ${ + // idToAsset(getValues('assetIn'))?.symbol || + // getValues('assetIn') + // }`; + return spotPrice?.outIn && assetIn ? ( + <> + + = + + + ) : ( + <>- + ); + } + })()} +
-
You get
{' '} !Object.values(assetIds).includes(asset.id))} + assets={assets?.filter( + (asset) => !Object.values(assetIds).includes(asset.id) + )} />{' '}
- {activeAccountTradeBalancesLoading || - isPoolLoading - ? ( +
You get
+ {activeAccountTradeBalancesLoading || isPoolLoading ? ( 'Your balance: loading' ) : ( // : `${fromPrecision12(tradeBalances.outBeforeTrade)} -> ${fromPrecision12(tradeBalances.outAfterTrade)}` <> Your balance: {assetIds.assetOut ? ( - tradeBalances.outBeforeTrade !== undefined - ? ( - - ) - : <> {horizontalBar} - ) : ( - <> {horizontalBar} - )} - {assetIds.assetOut && tradeBalances.outBeforeTrade !== undefined && tradeBalances.outAfterTrade !== undefined ? ( - <> - + tradeBalances.outBeforeTrade !== undefined ? ( - + ) : ( + <> {horizontalBar} + ) ) : ( - <> + <> {horizontalBar} )} - {tradeBalances.outTradeChange && - !tradeBalances.outTradeChange.isZero() && ( -
- ( - {tradeBalances.outTradeChange?.lt('0.01') - ? `< 0.01` - : tradeBalances.outTradeChange?.gt('1000') - ? `> 1000` - : tradeBalances.outTradeChange.toFixed(2)} - %) -
- )} )}
-
-
-
{ const assetOutAmount = getValues('assetOutAmount'); @@ -825,10 +969,7 @@ export const TradeForm = ({ }, maxTradeLimitIn: () => { const assetInAmount = getValues('assetInAmount'); - if (!assetInAmount || assetInAmount === '0') return false; - return new BigNumber(assetInLiquidity || '0') - .dividedBy(3) - .gte(assetInAmount); + return minTradeLimitIn(assetInAmount); }, slippageHigherThanTolerance: () => { if (!allowedSlippage) return false; @@ -838,24 +979,25 @@ export const TradeForm = ({ const assetIn = getValues('assetIn'); const assetInAmount = getValues('assetInAmount'); - let nativeAssetBalance = find(activeAccount?.balances, { - assetId: '0' - })?.balance; + if (!feePaymentAsset) return false; - let balanceForFee = nativeAssetBalance; + let feePaymentAssetBalance = find(activeAccount?.balances, { + assetId: feePaymentAsset, + })?.balance - if (assetIn === '0' && assetInAmount && nativeAssetBalance) { - balanceForFee = new BigNumber(nativeAssetBalance) + let balanceForFee = feePaymentAssetBalance; + + if (assetIn === feePaymentAsset && assetInAmount && feePaymentAssetBalance) { + balanceForFee = new BigNumber(feePaymentAssetBalance) .minus(assetInAmount) .toString(); } if (!paymentInfo) return true; if (!balanceForFee) return false; - - return new BigNumber(balanceForFee) - .gte(paymentInfo); - } + console.log('balance for free', balanceForFee, paymentInfo); + return new BigNumber(balanceForFee).gte(paymentInfo); + }, }, })} disabled={!isValid || tradeLoading || !isDirty} diff --git a/src/components/Trade/TradeForm/TradeInfo/TradeInfo.scss b/src/components/Trade/TradeForm/TradeInfo/TradeInfo.scss index 56c4cb98..399468a0 100644 --- a/src/components/Trade/TradeForm/TradeInfo/TradeInfo.scss +++ b/src/components/Trade/TradeForm/TradeInfo/TradeInfo.scss @@ -7,9 +7,9 @@ justify-content: center; gap: 4px; - min-height: 90px; - font-size: 12px; - font-weight: 600; + min-height: 100px; + font-size: 15px; + font-weight: 400; &__data { display: flex; @@ -17,7 +17,7 @@ justify-content: center; - max-height: 65px; + max-height: 120px; opacity: 1; transition: max-height 0.3s ease, opacity 0.15s ease; @@ -28,41 +28,48 @@ } .data-piece { + padding: 2px 0 4px 0; display: flex; justify-content: space-between; align-items: center; &__label { - color: #bdccd4; - font-weight: 700; + color: #9ea9b1; + } + position: relative; + + &:not(:last-child):after { + content: ' '; + position: absolute; + width: 100%; + height: 1px; + background-color: #26282f; + bottom: 0; } } } .validation { - opacity: 0.3; + opacity: 0; line-height: 16px; - height: 16px; + padding: 0 16px; + height: 100%; max-height: 0px; - // max-height: 30px; overflow: hidden; - transition: max-height 0.3s ease, opacity 0.3s ease; + transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease; + border-radius: 8px; &.visible { - max-height: 30px; + max-height: 80px; + padding: 16px; opacity: 1; } &.error { - font-size: 14px; - color: $red1; + background: rgba(255, 104, 104, 0.3); } &.warning { - max-height: 30px; - opacity: 1; - - font-size: 14px; color: $orange1; } } diff --git a/src/components/Trade/TradeForm/TradeInfo/TradeInfo.tsx b/src/components/Trade/TradeForm/TradeInfo/TradeInfo.tsx index ec1cd39b..bbda39d7 100644 --- a/src/components/Trade/TradeForm/TradeInfo/TradeInfo.tsx +++ b/src/components/Trade/TradeForm/TradeInfo/TradeInfo.tsx @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js'; import { debounce, delay, throttle } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { FieldErrors } from 'react-hook-form'; +import { useMultiFeePaymentConversionContext } from '../../../../containers/MultiProvider'; import { Balance, Fee } from '../../../../generated/graphql'; import { FormattedBalance } from '../../../Balance/FormattedBalance/FormattedBalance'; import { horizontalBar } from '../../../Chart/ChartHeader/ChartHeader'; @@ -16,7 +17,7 @@ export interface TradeInfoProps { isDirty?: boolean; expectedSlippage?: BigNumber; errors?: FieldErrors; - paymentInfo?: string, + paymentInfo?: string; } export const TradeInfo = ({ @@ -25,7 +26,7 @@ export const TradeInfo = ({ tradeLimit, isDirty, tradeFee = constants.xykFee, - paymentInfo + paymentInfo, }: TradeInfoProps) => { const [displayError, setDisplayError] = useState(); const isError = useMemo(() => !!errors?.submit?.type, [errors?.submit]); @@ -44,7 +45,11 @@ export const TradeInfo = ({ case 'notEnoughBalanceIn': return 'Insufficient balance'; case 'notEnoughFeeBalance': - return 'Insufficient fee balance' + return 'Insufficient fee balance'; + case 'poolDoesNotExist': + return 'Please select valid pool'; + case 'activeAccount': + return 'Please connect a wallet to continue'; } return; }, [errors?.submit]); @@ -58,16 +63,17 @@ export const TradeInfo = ({ return () => timeoutId && clearTimeout(timeoutId); }, [formError]); + const { feePaymentAsset } = useMultiFeePaymentConversionContext(); + return (
- Current slippage + Price impact
{!expectedSlippage || expectedSlippage?.isNaN() ? horizontalBar - : `${expectedSlippage?.multipliedBy(100).toFixed(2)}%` - } + : `${expectedSlippage?.multipliedBy(100).toFixed(2)}%`}
@@ -92,7 +98,7 @@ export const TradeInfo = ({ ) : ( diff --git a/src/components/Wallet/AccountSelector/AccountItem/AccountItem.scss b/src/components/Wallet/AccountSelector/AccountItem/AccountItem.scss index f963a678..fdda12de 100644 --- a/src/components/Wallet/AccountSelector/AccountItem/AccountItem.scss +++ b/src/components/Wallet/AccountSelector/AccountItem/AccountItem.scss @@ -2,22 +2,61 @@ @import '../../../../misc/misc.module.scss'; .account-item { - display: flex; - flex-direction: column; position: relative; - gap: 4px; - cursor: pointer; - padding: 8px 16px; - - border: 1px solid transparent; - border-radius: $border-radius; + border-radius: 12px; background-color: $gray3; - &--active { - background-color: $gray5; + padding: 1px; + + &--active, + &:hover { + background: linear-gradient(90deg, #4fffb0, #b3ff8f, #ff984e); + .account-item__wrapper { + &:before { + content: ' '; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + + background: linear-gradient( + 285.92deg, + rgba(73, 228, 159, 0) 25.46%, + rgba(228, 175, 73, 0.2) 98.29% + ), + rgba(76, 243, 168, 0.12); + + border-radius: 12px; + z-index: -1; + + &__chain-name { + color: $green1; + } + } + + background-color: #211f24; + z-index: 1; + } + } + + &__wrapper { + display: flex; + flex-direction: column; + gap: 16px; + + width: 100%; + height: 100%; + position: relative; + padding: 16px 16px; + + top: 0; + left: 0; + + border-radius: 12px; } &__address-entry { @@ -28,7 +67,7 @@ } &__address-info { - gap: 4px; + gap: 16px; display: flex; flex-direction: column; } @@ -43,7 +82,9 @@ &__heading { width: 100%; display: flex; - gap: 16px; + flex-direction: row; + flex-wrap: wrap; + gap: 5px; font-weight: 500; justify-content: space-between; @@ -51,14 +92,15 @@ display: flex; gap: 8px; align-items: center; + width: 100%; &__name { - flex-shrink: 100px; + font-size: 16px; + font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; - font-weight: 800; } &__source { @@ -73,44 +115,45 @@ } &:hover { - border-color: $green1; - - .account-item { - &__heading { - &__left { - &__source { - opacity: 1; - } - } + .account-item__wrapper { + &:before { + background: #26282f; + z-index: -1; } } } .account-item &__identicon { display: flex; - width: 40px; - height: 40px; + width: 32px; + height: 32px; line-height: 32px; flex-shrink: 0; justify-content: center; align-items: center; + background-color: black; + border-radius: 50%; + svg { + circle:first-child { + fill: black; + } + position: relative; - left: -2px; } } &__chain-name { font-size: 12px; - line-height: 1.1em; - font-weight: 500; + line-height: 1.2em; + font-weight: 400; } &__chain-address { - font-weight: normal; - font-size: 18px; - line-height: 1.1em; + font-weight: 600; + font-size: 14px; + line-height: 1.2em; overflow: hidden; text-overflow: ellipsis; } diff --git a/src/components/Wallet/AccountSelector/AccountItem/AccountItem.tsx b/src/components/Wallet/AccountSelector/AccountItem/AccountItem.tsx index 64507593..1db376fc 100644 --- a/src/components/Wallet/AccountSelector/AccountItem/AccountItem.tsx +++ b/src/components/Wallet/AccountSelector/AccountItem/AccountItem.tsx @@ -66,59 +66,70 @@ export const AccountItem = ({ account, onClick, active }: AccountItemProps) => { onClick(account); }} > -
-
-
- {account.name} -
-
- {sourceToHuman(account.source)} -
-
-
- {} -
-
-
-
- -
-
Basilisk
-
- {trimAddress(account.id, 24)} +
+
+
+
+ {account.name} +
+
+ {sourceToHuman(account.source)}
+
+ {} +
- {genesisHashToChain(account.genesisHash).network !== 'basilisk' ? ( -
+
+
e.stopPropagation()} + >
-
- {genesisHashToChain(account.genesisHash).displayName} -
+
Basilisk
- {trimAddress( - encodeAddress( - decodeAddress(account.id), - genesisHashToChain(account.genesisHash)?.prefix - ), - 24 - )} + {trimAddress(account.id, 24)}
- ) : ( - <> - )} + {genesisHashToChain(account.genesisHash).network !== 'basilisk' ? ( +
e.stopPropagation()} + > + +
+
+ {genesisHashToChain(account.genesisHash).displayName} +
+
+ {trimAddress( + encodeAddress( + decodeAddress(account.id), + genesisHashToChain(account.genesisHash)?.prefix + ), + 24 + )} +
+
+
+ ) : ( + <> + )} +
); diff --git a/src/components/Wallet/AccountSelector/AccountSelector.scss b/src/components/Wallet/AccountSelector/AccountSelector.scss index cf235964..b56880ce 100644 --- a/src/components/Wallet/AccountSelector/AccountSelector.scss +++ b/src/components/Wallet/AccountSelector/AccountSelector.scss @@ -7,34 +7,50 @@ justify-content: center; align-items: center; - padding: 16px; width: 100%; height: 100%; top: 0; left: 0; - background: rgba(50, 50, 50, 0.5); + color: white; z-index: 3; &__content-wrapper { - width: 460px; + width: 100%; + max-width: 460px; min-height: 500px; - max-height: 85vh; + max-height: 690px; border-radius: $border-radius; + + &__create-account-link { + text-decoration: none; + font-weight: normal; + color: $orange1; + } + + .account-selector__message { + padding: 16px; + + text-align: center; + + .account-selector__create-account-link { + display: inline; + } + } } &__clear-button { display: flex; justify-content: center; - padding-bottom: 8px; button { width: auto; color: $gray4; background: none; line-height: 16px; - padding-bottom: 8px; + padding: 16px; + width: 100%; &:hover { background: none; @@ -42,18 +58,4 @@ } } } - - &__create-account-link { - text-decoration: none; - font-weight: normal; - color: $orange1; - } - - &__message { - padding: 16px; - - text-align: center; - justify-content: center; - align-items: center; - } } diff --git a/src/components/Wallet/AccountSelector/AccountSelector.tsx b/src/components/Wallet/AccountSelector/AccountSelector.tsx index 277700b2..1e219c3d 100644 --- a/src/components/Wallet/AccountSelector/AccountSelector.tsx +++ b/src/components/Wallet/AccountSelector/AccountSelector.tsx @@ -1,5 +1,5 @@ import { MutableRefObject, useMemo } from 'react'; -import { Account } from '../../../generated/graphql'; +import { Account, Maybe } from '../../../generated/graphql'; import { AccountItem } from './AccountItem/AccountItem'; import { Button, ButtonKind } from '../../Button/Button'; import './AccountSelector.scss'; @@ -9,7 +9,7 @@ import Icon from '../../Icon/Icon'; export interface AccountSelectorProps { accounts?: Account[]; accountsLoading: boolean; - account?: Account; + account?: Maybe; onAccountSelected: (account: Account) => void; onAccountCleared: () => void; innerRef: MutableRefObject; @@ -38,15 +38,24 @@ export const AccountSelector = ({
{isExtensionAvailable ? ( - + <> +
+ +
+
+ Pick one of your accounts to connect to Basilisk +
+ ) : ( - +
+ +
)}
closeModal()}> @@ -76,24 +85,29 @@ export const AccountSelector = ({ ))}
) : ( - //TODO update href param when we know where to send user + //TODO update href param when we know where to send user
-
- -
-
- -
- - - -
+ + + + ), + }} + defaultMessage="Need help creating an account? {link}" + />
)} {account && ( diff --git a/src/components/Wallet/AccountSelector/hooks/useModalPortalElement.tsx b/src/components/Wallet/AccountSelector/hooks/useModalPortalElement.tsx index d5042fb0..6d9f93cb 100644 --- a/src/components/Wallet/AccountSelector/hooks/useModalPortalElement.tsx +++ b/src/components/Wallet/AccountSelector/hooks/useModalPortalElement.tsx @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { Account } from '../../../../generated/graphql'; +import { Account, Maybe } from '../../../../generated/graphql'; import { AccountSelector } from './../AccountSelector'; import { ModalPortalElementFactory, @@ -14,16 +14,15 @@ export type ModalPortalElement = ({ onAccountCleared, account, isExtensionAvailable, -}: Pick< - WalletProps, - | 'accounts' - | 'accountsLoading' - | 'onAccountSelected' - | 'onAccountCleared' - | 'account' - | 'isExtensionAvailable' ->) => ModalPortalElementFactory; -export type CloseModal = ModalPortalElementFactoryArgs['closeModal']; +}: { + accounts?: Account[], + accountsLoading: boolean, + account?: Maybe, + isExtensionAvailable: boolean, + onAccountSelected: (account: Account) => void, + onAccountCleared: () => void +}) => ModalPortalElementFactory; +export type CloseModal = ModalPortalElementFactoryArgs['closeModal']; export const useModalPortalElement: ModalPortalElement = ({ accounts, diff --git a/src/components/Wallet/Wallet.scss b/src/components/Wallet/Wallet.scss index 9026c826..6ded6ee4 100644 --- a/src/components/Wallet/Wallet.scss +++ b/src/components/Wallet/Wallet.scss @@ -3,17 +3,12 @@ .wallet { width: auto; - max-width: 550px; min-width: 350px; - - // flex-basis: 350px; - gap: 12px; - - background-color: $d-gray4; - border-radius: $border-radius; + border-radius: 8px; padding: 16px; + color: white; &__info { display: flex; @@ -50,7 +45,7 @@ &__account-btn { min-width: 90px; - padding: 8px 12px; + padding: 12px 16px; font-weight: 600; line-height: 16px; @@ -58,7 +53,7 @@ color: $l-gray2; background-color: $d-gray5; - border-radius: $border-radius; + border-radius: 9999px; text-align: center; diff --git a/src/components/Wallet/Wallet.stories.tsx b/src/components/Wallet/Wallet.stories.tsx index 9ddbbf9f..61c259e0 100644 --- a/src/components/Wallet/Wallet.stories.tsx +++ b/src/components/Wallet/Wallet.stories.tsx @@ -4,117 +4,117 @@ import { StorybookWrapper } from '../../misc/StorybookWrapper'; import { Wallet } from './Wallet'; import { toPrecision12 } from '../../hooks/math/useToPrecision'; -export default { - title: 'components/Wallet', - component: Wallet, - args: { - extensionLoading: false, - isExtensionAvailable: true, - account: { - name: 'LOOOOOOOONG snekmaster sdkaoskaodkosadkassdksadkoajdjdaosdjasoj', - balances: [ - { assetId: '0', balance: toPrecision12('100213') }, - { assetId: '1', balance: toPrecision12('300213') }, - ], - id: 'E7ncQKp4xayUoUdpraxBjT7NzLoayLJA4TuPcKKboBkJ5GH', - isActive: true, - vestingSchedule: {}, - source: 'polkadot-js', - }, - accounts: [ - { - name: 'Alice 1', - balances: [{ assetId: '0', balance: toPrecision12('100213') }], - id: 'E7ncQKp4xayUoUdpraxBjT7NzLoayLJA4TuPcKKboBkJ5GH', - isActive: true, - vestingSchedule: {}, - source: 'polkadot-js', - }, - { - name: 'Kusama snekmaster', - balances: [], - id: 'E7ncQKp4xayUoUdpraxBjT7NzxaayLJA4TuPcKKboBkJ5GH', - isActive: false, - vestingSchedule: {}, - source: 'polkadot-js', - }, - { - name: 'Kusama snekmaster', - balances: [{ assetId: '2', balance: toPrecision12('1') }], - id: 'E7ncQKp4xayUoUdpraxBjT7NzxaayLJA4TuPcKKboBkJ5GH', - isActive: false, - vestingSchedule: {}, - source: 'polkadot-js', - }, - { - name: 'LOOOOOOOONG snekmaster sdkaoskaodkosadkassdksadkoajdjdaosdjasoj', - balances: [ - { - assetId: '0', - balance: toPrecision12('10010101001000003203302023'), - }, - ], - id: 'E7ncQKp4xayUoUdpraxBjT7NzxaayLJA4TuPcKKboBkJ5GH', - isActive: false, - vestingSchedule: {}, - source: 'polkadot-js', - }, - ], - accountsLoading: false, - onAccountSelected: () => { - return Promise.resolve(); - }, - onAccountCleared: () => { - return Promise.resolve(); - }, - setAccountSelectorOpen: () => { - console.log('toggle modal open'); - }, - }, -} as ComponentMeta; +// export default { +// title: 'components/Wallet', +// component: Wallet, +// args: { +// extensionLoading: false, +// isExtensionAvailable: true, +// account: { +// name: 'LOOOOOOOONG snekmaster sdkaoskaodkosadkassdksadkoajdjdaosdjasoj', +// balances: [ +// { assetId: '0', balance: toPrecision12('100213') }, +// { assetId: '1', balance: toPrecision12('300213') }, +// ], +// id: 'E7ncQKp4xayUoUdpraxBjT7NzLoayLJA4TuPcKKboBkJ5GH', +// isActive: true, +// vestingSchedule: {}, +// source: 'polkadot-js', +// }, +// accounts: [ +// { +// name: 'Alice 1', +// balances: [{ assetId: '0', balance: toPrecision12('100213') }], +// id: 'E7ncQKp4xayUoUdpraxBjT7NzLoayLJA4TuPcKKboBkJ5GH', +// isActive: true, +// vestingSchedule: {}, +// source: 'polkadot-js', +// }, +// { +// name: 'Kusama snekmaster', +// balances: [], +// id: 'E7ncQKp4xayUoUdpraxBjT7NzxaayLJA4TuPcKKboBkJ5GH', +// isActive: false, +// vestingSchedule: {}, +// source: 'polkadot-js', +// }, +// { +// name: 'Kusama snekmaster', +// balances: [{ assetId: '2', balance: toPrecision12('1') }], +// id: 'E7ncQKp4xayUoUdpraxBjT7NzxaayLJA4TuPcKKboBkJ5GH', +// isActive: false, +// vestingSchedule: {}, +// source: 'polkadot-js', +// }, +// { +// name: 'LOOOOOOOONG snekmaster sdkaoskaodkosadkassdksadkoajdjdaosdjasoj', +// balances: [ +// { +// assetId: '0', +// balance: toPrecision12('10010101001000003203302023'), +// }, +// ], +// id: 'E7ncQKp4xayUoUdpraxBjT7NzxaayLJA4TuPcKKboBkJ5GH', +// isActive: false, +// vestingSchedule: {}, +// source: 'polkadot-js', +// }, +// ], +// accountsLoading: false, +// onAccountSelected: () => { +// return Promise.resolve(); +// }, +// onAccountCleared: () => { +// return Promise.resolve(); +// }, +// setAccountSelectorOpen: () => { +// console.log('toggle modal open'); +// }, +// }, +// } as ComponentMeta; -const Template: ComponentStory = (args) => { - const modalContainerRef = useRef(null); +// const Template: ComponentStory = (args) => { +// const modalContainerRef = useRef(null); - return ( - -
- {/* This is where the underlying modal should be rendered */} -
+// return ( +// +//
+// {/* This is where the underlying modal should be rendered */} +//
- {/* - Pass the ref to the element above, so that the Wallet - can render the modal there. - */} -
- -
-
- - ); -}; +// {/* +// Pass the ref to the element above, so that the Wallet +// can render the modal there. +// */} +//
+// +//
+//
+//
+// ); +// }; -export const Default = Template.bind({}); -export const NoAccountConnected = Template.bind({}); -NoAccountConnected.args = { - account: undefined, -}; -export const AccountsLoading = Template.bind({}); -AccountsLoading.args = { - accountsLoading: true, -}; -export const NoAccountsAvailable = Template.bind({}); -NoAccountsAvailable.args = { - account: undefined, - accounts: [], -}; -export const ExtensionUnavailable = Template.bind({}); -ExtensionUnavailable.args = { - isExtensionAvailable: false, - account: undefined, - accounts: [], -}; -export const LoadingData = Template.bind({}); -LoadingData.args = { - extensionLoading: true, -}; +// export const Default = Template.bind({}); +// export const NoAccountConnected = Template.bind({}); +// NoAccountConnected.args = { +// account: undefined, +// }; +// export const AccountsLoading = Template.bind({}); +// AccountsLoading.args = { +// accountsLoading: true, +// }; +// export const NoAccountsAvailable = Template.bind({}); +// NoAccountsAvailable.args = { +// account: undefined, +// accounts: [], +// }; +// export const ExtensionUnavailable = Template.bind({}); +// ExtensionUnavailable.args = { +// isExtensionAvailable: false, +// account: undefined, +// accounts: [], +// }; +// export const LoadingData = Template.bind({}); +// LoadingData.args = { +// extensionLoading: true, +// }; diff --git a/src/components/Wallet/Wallet.tsx b/src/components/Wallet/Wallet.tsx index c8043398..3d331f8b 100644 --- a/src/components/Wallet/Wallet.tsx +++ b/src/components/Wallet/Wallet.tsx @@ -1,6 +1,6 @@ -import { MutableRefObject, useCallback, useEffect } from 'react'; +import { Dispatch, MutableRefObject, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedBalance } from '../Balance/FormattedBalance/FormattedBalance'; -import { Account } from '../../generated/graphql'; +import { Account, Maybe } from '../../generated/graphql'; import Icon from '../Icon/Icon'; import Identicon from '@polkadot/react-identicon'; import './Wallet.scss'; @@ -21,14 +21,10 @@ export const trimAddress = (address: string, length: number) => { export interface WalletProps { modalContainerRef: MutableRefObject; - accounts?: Account[]; - accountsLoading: boolean; - account?: Account; - onAccountSelected: (account: Account) => void; - onAccountCleared: () => void; + account?: Maybe; extensionLoading: boolean; isExtensionAvailable: boolean; - setAccountSelectorOpen: (isModalOpen: boolean) => void; + onToggleAccountSelector: () => void, activeAccountLoading: boolean; faucetMint: () => void; faucetMintLoading?: boolean; @@ -36,43 +32,20 @@ export interface WalletProps { export const Wallet = ({ modalContainerRef, - accounts, - accountsLoading, account, - onAccountSelected, - onAccountCleared, extensionLoading, isExtensionAvailable, - setAccountSelectorOpen, + onToggleAccountSelector, activeAccountLoading, faucetMint, faucetMintLoading, }: WalletProps) => { - const modalPortalElement = useModalPortalElement({ - accounts, - accountsLoading, - onAccountSelected, - onAccountCleared, - account, - isExtensionAvailable, - }); - const { isModalOpen, toggleModal, modalPortal, toggleId } = useModalPortal( - modalPortalElement, - modalContainerRef, - false // don't auto close when clicking outside the modalPortalElement - ); - const handleAccountSelectorClick = useCallback(() => toggleModal(), [ - toggleModal, - ]); - - useEffect(() => { - setAccountSelectorOpen(isModalOpen); - }, [isModalOpen, setAccountSelectorOpen]); + const handleAccountSelectorClick = useMemo(() => ( + onToggleAccountSelector + ),[onToggleAccountSelector]); return (
- {/* This portal will be rendered at it's container ref as defined above */} - {modalPortal} {/*
{account ? ( @@ -91,7 +64,7 @@ export const Wallet = ({ <> )}
*/} -
+
{extensionLoading || activeAccountLoading ? (
{ + return useRef(null); +}; + +export const [BodyContainerRefProvider, useBodyContainerRefContext] = constate(useBodyContainerRef); + +export const BodyContainer = ({ children }: { children: React.ReactNode }) => { + const bodyContainerRef = useBodyContainerRefContext() + return
{children}
+}; + +// const [BodyContainerProvider, useBodyContainerContext] = constate(useBodyContainer); + +export const useMultiFeePaymentConversion = () => { + const { data: activeAccount } = useGetActiveAccountQueryContext() + const { data } = useGetConfigQuery({ + skip: !activeAccount?.activeAccount?.id + }); + + const feePaymentAsset = useMemo(() => data?.config.feePaymentAsset, [data]); + + const depsLoading = useLoading(); + const { + data: poolData, + loading: poolLoading, + networkStatus: poolNetworkStatus, + } = useGetPoolByAssetsQuery( + { + assetInId: '0', + assetOutId: feePaymentAsset || undefined, + }, + !activeAccount?.activeAccount?.id + ); + + const { math } = useMath() + + const convertToFeePaymentAsset = useCallback((txFee?: string) => { + console.log('convertToFeePaymentAsset', txFee, feePaymentAsset); + if (!txFee || poolLoading || !math) return; + if (feePaymentAsset === '0') return txFee; + + const liquidityAssetIn = poolData?.pool.balances?.find(balance => balance.assetId == '0')?.balance + const liquidityAssetOut = poolData?.pool.balances?.find(balance => balance.assetId == feePaymentAsset)?.balance + + if (!liquidityAssetIn || !liquidityAssetOut) return; + + const spotPrice = math?.xyk.get_spot_price( + liquidityAssetIn, + liquidityAssetOut, + '1000000000000' + ) + + if (!spotPrice) return; + + return new BigNumber(spotPrice) + .dividedBy( + new BigNumber(10).pow(12) + ) + .multipliedBy(txFee) + .toFixed(2) + }, [poolData, poolLoading, feePaymentAsset, math]); + + return { convertToFeePaymentAsset, feePaymentAsset } +} + +export const [MultiFeePaymentConversionProvider, useMultiFeePaymentConversionContext] = constate(useMultiFeePaymentConversion); + + export const QueryProvider = ({ children }: { children: React.ReactNode }) => ( - <>{children} + + + + <>{children} + + + ); // TODO: use react-multi-provider instead of ugly nesting export const MultiProvider = ({ children }: { children: React.ReactNode }) => { return ( - - - - - {children} - - - - + + + + + + + {children} + + + + + + ); }; diff --git a/src/containers/PageContainer.scss b/src/containers/PageContainer.scss index a95153e3..6dc8d318 100644 --- a/src/containers/PageContainer.scss +++ b/src/containers/PageContainer.scss @@ -7,12 +7,9 @@ position: relative; width: 100%; - padding: 16px; gap: 36px; - max-width: 1100px; - left: 0; right: 0; margin: auto; @@ -24,6 +21,10 @@ gap: 10px; + background: rgba(28, 26, 31, 0.2); + padding: 0 36px; + width: 100%; + &__wallet-wrapper { display: flex; align-items: center; @@ -63,6 +64,27 @@ } } } + + &__menu-wrapper { + display: flex; + flex-grow: 1; + gap: 20px; + padding-left: 24px; + &__menu-item { + a { + cursor: pointer; + font-weight: 700; + color: $l-gray2; + text-decoration: none; + &:visited { + color: $l-gray2; + } + &:hover { + color: $green1; + } + } + } + } } .footer { @@ -70,6 +92,7 @@ justify-content: center; align-items: center; flex-direction: column; + padding-bottom: 50px; color: #bdccd4; font-weight: 400; diff --git a/src/containers/PageContainer.tsx b/src/containers/PageContainer.tsx index 67ba5fd9..5686d8a4 100644 --- a/src/containers/PageContainer.tsx +++ b/src/containers/PageContainer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useLastBlockQuery } from '../hooks/lastBlock/useLastBlockQuery'; -import { Wallet } from './Wallet'; +import { Wallet } from './Wallet/Wallet'; import Icon from '../components/Icon/Icon'; import './PageContainer.scss'; import moment from 'moment'; @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { NetworkStatus } from '@apollo/client'; import { horizontalBar } from '../components/Chart/ChartHeader/ChartHeader'; import { useDebugBoxContext } from '../pages/TradePage/hooks/useDebugBox'; +import { Link } from 'react-router-dom'; export const PageContainer = ({ children }: { children: React.ReactNode }) => { const { data: lastBlockData } = useLastBlockQuery(); @@ -38,6 +39,23 @@ export const PageContainer = ({ children }: { children: React.ReactNode }) => {
+
+
+ + Trade + +
+
+ + Wallet + +
+
+ + Pools + +
+
{ return ( } /> + } /> + } /> } /> ); diff --git a/src/containers/Wallet.tsx b/src/containers/Wallet.tsx deleted file mode 100644 index 692b3687..00000000 --- a/src/containers/Wallet.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Wallet as WalletComponent } from '../components/Wallet/Wallet'; -import { useCallback, useRef, useState } from 'react'; -import { useGetAccountsQuery } from '../hooks/accounts/queries/useGetAccountsQuery'; -import { useGetExtensionQuery } from '../hooks/extension/queries/useGetExtensionQuery'; -import { useSetActiveAccountMutation } from '../hooks/accounts/mutations/useSetActiveAccountMutation'; -import { useGetActiveAccountQuery } from '../hooks/accounts/queries/useGetActiveAccountQuery'; -import { Account } from '../generated/graphql'; -import { NetworkStatus } from '@apollo/client'; -import { useLoading } from '../hooks/misc/useLoading'; -import { useFaucetMintMutation } from '../hooks/faucet/mutations/useFaucetMintMutation'; - -export const Wallet = () => { - const { data: extensionData, loading: extensionLoading } = - useGetExtensionQuery(); - const [setActiveAccount] = useSetActiveAccountMutation(); - const depsLoading = useLoading(); - const { data: activeAccountData, networkStatus: activeAccountNetworkStatus } = useGetActiveAccountQuery({ - skip: depsLoading - }); - const [isAccountSelectorOpen, setAccountSelectorOpen] = useState(false); - const { data: accountsData, loading: accountsLoading, networkStatus: accountsNetworkStatus } = useGetAccountsQuery( - !(extensionData?.extension.isAvailable && isAccountSelectorOpen) || depsLoading - ); - - const modalContainerRef = useRef(null); - - const onAccountSelected = useCallback( - (account: Account) => { - setActiveAccount({ variables: { id: account.id } }); - }, - [setActiveAccount] - ); - - const onAccountCleared = useCallback(() => { - setActiveAccount({ variables: { id: undefined } }); - }, [setActiveAccount]); - - const [faucetMint, { loading: faucetMintLoading }] = useFaucetMintMutation(); - - // request data from the data layer - // render the component with the provided data - return ( - <> -
- faucetMint()} - faucetMintLoading={faucetMintLoading} - /> - - ); -}; diff --git a/src/containers/Wallet/Wallet.tsx b/src/containers/Wallet/Wallet.tsx new file mode 100644 index 00000000..9db34980 --- /dev/null +++ b/src/containers/Wallet/Wallet.tsx @@ -0,0 +1,46 @@ +import { Wallet as WalletComponent } from '../../components/Wallet/Wallet'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useGetAccountsQuery } from '../../hooks/accounts/queries/useGetAccountsQuery'; +import { useGetExtensionQuery, useGetExtensionQueryContext } from '../../hooks/extension/queries/useGetExtensionQuery'; +import { useSetActiveAccountMutation } from '../../hooks/accounts/mutations/useSetActiveAccountMutation'; +import { useGetActiveAccountQuery, useGetActiveAccountQueryContext } from '../../hooks/accounts/queries/useGetActiveAccountQuery'; +import { Account } from '../../generated/graphql'; +import { NetworkStatus } from '@apollo/client'; +import { useLoading } from '../../hooks/misc/useLoading'; +import { useFaucetMintMutation } from '../../hooks/faucet/mutations/useFaucetMintMutation'; +import { useAccountSelectorModal } from './hooks/useAccountSelectorModal'; + +export const Wallet = () => { + const { data: extensionData, loading: extensionLoading } = + useGetExtensionQueryContext(); + const [setActiveAccount] = useSetActiveAccountMutation(); + const depsLoading = useLoading(); + const { data: activeAccountData, networkStatus: activeAccountNetworkStatus } = useGetActiveAccountQueryContext(); + + const modalContainerRef = useRef(null); + + const [faucetMint, { loading: faucetMintLoading }] = useFaucetMintMutation(); + + const { modalPortal, toggleModal, isModalOpen } = useAccountSelectorModal({ + modalContainerRef + }); + + // request data from the data layer + // render the component with the provided data + return ( + <> +
+ {modalPortal} + faucetMint()} + faucetMintLoading={faucetMintLoading} + /> + + ); +}; diff --git a/src/containers/Wallet/hooks/useAccountSelectorModal.tsx b/src/containers/Wallet/hooks/useAccountSelectorModal.tsx new file mode 100644 index 00000000..d63015b2 --- /dev/null +++ b/src/containers/Wallet/hooks/useAccountSelectorModal.tsx @@ -0,0 +1,61 @@ +import { NetworkStatus } from "@apollo/client"; +import { MutableRefObject, useCallback, useEffect, useState } from "react"; +import { useModalPortal } from "../../../components/Balance/AssetBalanceInput/hooks/useModalPortal"; +import { useModalPortalElement } from "../../../components/Wallet/AccountSelector/hooks/useModalPortalElement"; +import { Account } from "../../../generated/graphql"; +import { useSetActiveAccountMutation } from "../../../hooks/accounts/mutations/useSetActiveAccountMutation"; +import { useGetAccountsLazyQuery, useGetAccountsQuery } from "../../../hooks/accounts/queries/useGetAccountsQuery"; +import { useGetActiveAccountQuery, useGetActiveAccountQueryContext } from "../../../hooks/accounts/queries/useGetActiveAccountQuery"; +import { useGetExtensionQuery, useGetExtensionQueryContext } from "../../../hooks/extension/queries/useGetExtensionQuery"; +import { useLoading } from "../../../hooks/misc/useLoading"; + +export const useAccountSelectorModal = ({ + modalContainerRef, +}: { + modalContainerRef: MutableRefObject, +}) => { + const { data: extensionData, loading: extensionLoading } = + useGetExtensionQueryContext(); + const [setActiveAccount] = useSetActiveAccountMutation(); + const depsLoading = useLoading(); + const { data: activeAccountData, networkStatus: activeAccountNetworkStatus } = + useGetActiveAccountQueryContext() + const [getAccounts, { + data: accountsData, + networkStatus: accountsNetworkStatus, + }] = useGetAccountsLazyQuery(); + + const onAccountSelected = useCallback( + (account: Account) => { + setActiveAccount({ variables: { id: account.id } }); + }, + [setActiveAccount] + ); + + const onAccountCleared = useCallback(() => { + setActiveAccount({ variables: { id: undefined } }); + }, [setActiveAccount]); + + const modalPortalElement = useModalPortalElement({ + accounts: accountsData?.accounts, + accountsLoading: accountsNetworkStatus === NetworkStatus.loading, + onAccountSelected, + onAccountCleared, + account: activeAccountData?.activeAccount, + isExtensionAvailable: !!extensionData?.extension.isAvailable, + }); + + const modal = useModalPortal( + modalPortalElement, + modalContainerRef, + // TODO: this doesnt work anyhow due to the backdrop + // being included in the outside-click detection + false // don't auto close when clicking outside the modalPortalElement + ); + + useEffect(() => { + extensionData?.extension.isAvailable && !depsLoading && modal.isModalOpen && getAccounts(); + }, [modal.isModalOpen, extensionData, depsLoading, getAccounts]) + + return modal; +}; diff --git a/src/errors.tsx b/src/errors.tsx index 9f3d0975..31848b86 100644 --- a/src/errors.tsx +++ b/src/errors.tsx @@ -7,4 +7,5 @@ export default { 'One or more arguments missing to the locked balance query', invalidTransferVariables: 'Invalid transfer parameters provided', usableBalanceNotAvailable: 'Unable to determine usable balance', + vestingScheduleIncomplete: 'Vesting schedule has at least one undefined property' }; diff --git a/src/generated/graphql.tsx b/src/generated/graphql.tsx index 99fdbec0..30c8f914 100644 --- a/src/generated/graphql.tsx +++ b/src/generated/graphql.tsx @@ -13,13 +13,19 @@ export type Scalars = { Float: number; }; -export type Account = { +export type Account = Balances & IVesting & { __typename?: 'Account'; balances: Array; genesisHash?: Maybe; id: Scalars['String']; name?: Maybe; source?: Maybe; + vesting: Vesting; +}; + + +export type AccountBalancesArgs = { + assetIds?: InputMaybe>>; }; export type Asset = { @@ -40,6 +46,20 @@ export type Balance = { id?: Maybe; }; +export type Balances = { + balances: Array; +}; + + +export type BalancesBalancesArgs = { + assetIds?: InputMaybe>>; +}; + +export enum ChromeExtension { + Polkadotjs = 'POLKADOTJS', + Talisman = 'TALISMAN' +} + export type Config = { __typename?: 'Config'; appName: Scalars['String']; @@ -50,7 +70,7 @@ export type Config = { export type Extension = { __typename?: 'Extension'; - extension?: Maybe; + extension?: Maybe; id: Scalars['String']; isAvailable: Scalars['Boolean']; }; @@ -67,6 +87,10 @@ export type FeePaymentAsset = { fallbackPrice?: Maybe; }; +export type IVesting = { + vesting?: Maybe; +}; + export type LbpAssetWeights = { __typename?: 'LBPAssetWeights'; current: Scalars['String']; @@ -103,15 +127,22 @@ export type LockedBalance = { lockId: Scalars['String']; }; -export type Pool = LbpPool | XykPool; +export type Mutation = { + __typename?: 'Mutation'; + _empty?: Maybe; + setActiveAccount?: Maybe; +}; + +export type Pool = XykPool; -export type Query = { +export type Query = Balances & IVesting & { __typename?: 'Query'; _assetIds?: Maybe; _empty?: Maybe; _tradeType?: Maybe; + _vestingSchedule?: Maybe; accounts: Array; - activeAccount: Account; + activeAccount?: Maybe; assets?: Maybe>; balances: Array; config: Config; @@ -119,9 +150,16 @@ export type Query = { feePaymentAssets?: Maybe>; lastBlock?: Maybe; lockedBalances: Array; - pools?: Maybe>; + pools: Array; + vesting?: Maybe; +}; + + +export type QueryBalancesArgs = { + assetIds?: InputMaybe>>; }; + export type QueryLockedBalancesArgs = { address?: InputMaybe; lockId: Scalars['String']; @@ -132,10 +170,27 @@ export enum TradeType { Sell = 'Sell' } +export type Vesting = { + __typename?: 'Vesting'; + claimableAmount: Scalars['String']; + lockedVestingBalance: Scalars['String']; + originalLockBalance: Scalars['String']; +}; + +export type VestingSchedule = { + __typename?: 'VestingSchedule'; + perPeriod: Scalars['String']; + period: Scalars['String']; + periodCount: Scalars['String']; + start: Scalars['String']; +}; + export type XykPool = { __typename?: 'XYKPool'; assetInId: Scalars['String']; assetOutId: Scalars['String']; balances?: Maybe>; id: Scalars['String']; + shareTokenId: Scalars['String']; + totalLiquidity: Scalars['String']; }; diff --git a/src/hooks/accounts/graphql/Accounts.graphql b/src/hooks/accounts/graphql/Accounts.graphql index fafee6d3..9b6957df 100644 --- a/src/hooks/accounts/graphql/Accounts.graphql +++ b/src/hooks/accounts/graphql/Accounts.graphql @@ -1,18 +1,18 @@ #import "./../../balances/graphql/Balance.graphql" -#import './../../vesting/graphql/VestingSchedule.graphql' +#import './../../vesting/graphql/Vesting.graphql' -type Account implements Balances { +type Account implements Balances & IVesting { id: String! name: String source: String genesisHash: String - # TODO: Can the balances query definition be re-used here? balances(assetIds: [String]): [Balance!]! + vesting: Vesting! } extend type Query { accounts: [Account!]! - activeAccount: Account! + activeAccount: Account } extend type Mutation { diff --git a/src/hooks/accounts/graphql/GetActiveAccount.query.graphql b/src/hooks/accounts/graphql/GetActiveAccount.query.graphql index 5ab05497..4a0c06c4 100644 --- a/src/hooks/accounts/graphql/GetActiveAccount.query.graphql +++ b/src/hooks/accounts/graphql/GetActiveAccount.query.graphql @@ -9,9 +9,14 @@ query GetActiveAccount { id name source - balances(assetIds: ["0"]) { + balances { assetId balance + }, + vesting { + claimableAmount, + originalLockBalance, + lockedVestingBalance } } } diff --git a/src/hooks/accounts/lib/getAccounts.test.tsx b/src/hooks/accounts/lib/getAccounts.test.tsx index 6acd942d..bc228f8e 100644 --- a/src/hooks/accounts/lib/getAccounts.test.tsx +++ b/src/hooks/accounts/lib/getAccounts.test.tsx @@ -40,7 +40,7 @@ describe('getAccounts', () => { }); it('can retrieve one account', async () => { - const accounts: Account[] = await getAccounts(); + const accounts: Partial[] = await getAccounts(); expect(accounts).toEqual([ { @@ -76,7 +76,7 @@ describe('getAccounts', () => { }); it('can retrieve multiple accounts', async () => { - const accounts: Account[] = await getAccounts(); + const accounts: Partial[] = await getAccounts(); expect(accounts).toEqual([ { @@ -104,7 +104,7 @@ describe('getAccounts', () => { }); it('returns an empty array when no accounts are returned from wallet', async () => { - const accounts: Account[] = await getAccounts(); + const accounts: Partial[] = await getAccounts(); expect(accounts).toEqual([]); expect(mockWeb3Accounts).toHaveBeenCalledTimes(1); diff --git a/src/hooks/accounts/lib/getAccounts.tsx b/src/hooks/accounts/lib/getAccounts.tsx index 28b6cdf4..d49cc38e 100644 --- a/src/hooks/accounts/lib/getAccounts.tsx +++ b/src/hooks/accounts/lib/getAccounts.tsx @@ -7,7 +7,7 @@ import { encodeAddress, decodeAddress } from '@polkadot/util-crypto'; * Used to fetch all accounts * @returns an array of accounts in required format */ -export const getAccounts = async (): Promise => { +export const getAccounts = async (): Promise[]> => { // ensure we're connected to the polkadot.js extension await web3Enable(constants.basiliskWeb3ProviderName); @@ -15,8 +15,6 @@ export const getAccounts = async (): Promise => { // return all retrieved accounts const accounts = await web3Accounts(); - console.log('accounts', accounts); - // transform the returned accounts into the required entity format return accounts.map((account) => { return { @@ -24,7 +22,6 @@ export const getAccounts = async (): Promise => { name: account.meta.name, source: account.meta.source, genesisHash: account.meta.genesisHash || null, - balances: [], }; }); }; diff --git a/src/hooks/accounts/queries/useGetAccountsQuery.tsx b/src/hooks/accounts/queries/useGetAccountsQuery.tsx index 6974cc26..5c439bf1 100644 --- a/src/hooks/accounts/queries/useGetAccountsQuery.tsx +++ b/src/hooks/accounts/queries/useGetAccountsQuery.tsx @@ -1,4 +1,4 @@ -import { useQuery } from '@apollo/client'; +import { useLazyQuery, useQuery } from '@apollo/client'; import { Query } from '../../../generated/graphql'; import { loader } from 'graphql.macro'; @@ -13,3 +13,9 @@ export const useGetAccountsQuery = (skip: boolean = false) => notifyOnNetworkStatusChange: true, skip: skip, }); + +export const useGetAccountsLazyQuery = () => + useLazyQuery(GET_ACCOUNTS, { + notifyOnNetworkStatusChange: true, + fetchPolicy: 'cache-only' + }); diff --git a/src/hooks/accounts/queries/useGetActiveAccountQuery.tsx b/src/hooks/accounts/queries/useGetActiveAccountQuery.tsx index 51db4f1f..007e5a10 100644 --- a/src/hooks/accounts/queries/useGetActiveAccountQuery.tsx +++ b/src/hooks/accounts/queries/useGetActiveAccountQuery.tsx @@ -1,6 +1,9 @@ import { QueryHookOptions, useQuery } from '@apollo/client'; +import constate from 'constate'; import { loader } from 'graphql.macro'; -import { Query } from '../../../generated/graphql'; +import { Query, Vesting } from '../../../generated/graphql'; +import { useGetExtensionQuery, useGetExtensionQueryContext } from '../../extension/queries/useGetExtensionQuery'; +import { useLoading } from '../../misc/useLoading'; // graphql query export const GET_ACTIVE_ACCOUNT = loader( @@ -9,7 +12,7 @@ export const GET_ACTIVE_ACCOUNT = loader( // data shape returned from the query export interface GetActiveAccountQueryResponse { - activeAccount: Query['activeAccount']; + activeAccount: Query['activeAccount'] } // hook wrapping the built-in apollo useQuery hook with proper types & configuration @@ -18,3 +21,12 @@ export const useGetActiveAccountQuery = (options?: QueryHookOptions) => notifyOnNetworkStatusChange: true, ...options }); + + +export const [GetActiveAccountQueryProvider, useGetActiveAccountQueryContext] = constate(() => { + const depsLoading = useLoading(); + const { loading: extensionLoading } = useGetExtensionQueryContext(); + return useGetActiveAccountQuery({ + skip: depsLoading || extensionLoading, + }) +}); \ No newline at end of file diff --git a/src/hooks/accounts/resolvers/query/accounts.tsx b/src/hooks/accounts/resolvers/query/accounts.tsx index 2b7ae87a..5a648889 100644 --- a/src/hooks/accounts/resolvers/query/accounts.tsx +++ b/src/hooks/accounts/resolvers/query/accounts.tsx @@ -9,8 +9,6 @@ export const useAccountsQueryResolver = () => { useCallback(async (_obj) => { const accounts = await getAccounts(); - console.log('got accounts', accounts); - // if no results were found, return undefined/null // this is useful when un-setting the active account if (!accounts) { diff --git a/src/hooks/accounts/resolvers/query/activeAccount.tsx b/src/hooks/accounts/resolvers/query/activeAccount.tsx index f53c1cdc..733a152c 100644 --- a/src/hooks/accounts/resolvers/query/activeAccount.tsx +++ b/src/hooks/accounts/resolvers/query/activeAccount.tsx @@ -10,6 +10,7 @@ import { withErrorHandler } from '../../../apollo/withErrorHandler'; import { withTypename } from '../../types'; import { Account } from '../../../../generated/graphql'; +// TODO: turn the active account into a cache ref to Account export const activeAccountQueryResolverFactory = (persistedActiveAccount?: PersistedAccount) => /** @@ -26,7 +27,7 @@ export const activeAccountQueryResolverFactory = _obj: any, _args: any, { client }: { client: ApolloClient } - ): Promise => { + ): Promise | null> => { if (persistedActiveAccount?.id) { const { data: accountsData } = await client.query({ query: GET_ACCOUNTS, diff --git a/src/hooks/accounts/types.tsx b/src/hooks/accounts/types.tsx index 56bf615e..58d1de37 100644 --- a/src/hooks/accounts/types.tsx +++ b/src/hooks/accounts/types.tsx @@ -4,7 +4,7 @@ import { Account } from '../../generated/graphql'; const __typename: Account['__typename'] = 'Account'; // helper function to decorate the extension entity for normalised caching -export const withTypename = (account: Account) => ({ +export const withTypename = (account: Partial) => ({ __typename, ...account, }); diff --git a/src/hooks/actionLog/useIntentions.tsx b/src/hooks/actionLog/useIntentions.tsx deleted file mode 100644 index 83efa12b..00000000 --- a/src/hooks/actionLog/useIntentions.tsx +++ /dev/null @@ -1 +0,0 @@ -export const useIntentions = () => {}; \ No newline at end of file diff --git a/src/hooks/actionLog/useWithConfirmation.tsx b/src/hooks/actionLog/useWithConfirmation.tsx new file mode 100644 index 00000000..0d0a5e78 --- /dev/null +++ b/src/hooks/actionLog/useWithConfirmation.tsx @@ -0,0 +1,54 @@ +import { MutationTuple } from '@apollo/client'; +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import { ModalPortalElementFactoryArgs, useModalPortal } from '../../components/Balance/AssetBalanceInput/hooks/useModalPortal'; +import { Confirmation } from '../../components/Confirmation/Confirmation'; +import { useBodyContainerRefContext } from '../../containers/MultiProvider'; + +export enum ConfirmationType { + Trade +} + +export const useWithConfirmation = < + TData extends unknown, + TVariables extends unknown +>( + mutationTuple: MutationTuple, + confirmationType: ConfirmationType +): { + mutation: MutationTuple; + confirmationScreen: ReactNode; +} => { + const [submit] = mutationTuple; + // TODO: figure out a way to type this properly + const [options, setOptions] = useState(); + const bodyContainerRef = useBodyContainerRefContext(); + + const { openModal, closeModal, modalPortal, status } = useModalPortal( + useCallback((args: ModalPortalElementFactoryArgs) => { + console.log('options', options); + return + }, [options, confirmationType]), + bodyContainerRef + ); + + useEffect(() => { + status === 'success' && submit(options) + }, [status, submit, options]); + + const submitWithConfirmation = useCallback( + async (options: Parameters[0]) => { + openModal(); + setOptions(options); + }, + [] + ); + + return { + mutation: [submitWithConfirmation as any, mutationTuple[1]], + confirmationScreen: modalPortal, + }; +}; diff --git a/src/hooks/apollo/useApollo.tsx b/src/hooks/apollo/useApollo.tsx index bcf4a9a4..5d380900 100644 --- a/src/hooks/apollo/useApollo.tsx +++ b/src/hooks/apollo/useApollo.tsx @@ -13,6 +13,9 @@ import { usePoolsMutationResolvers } from '../pools/resolvers/usePoolsMutationRe import { useExtensionResolvers } from '../extension/resolvers/useExtensionResolvers'; import { usePersistentConfig } from '../config/usePersistentConfig'; import { useFaucetResolvers } from '../faucet/resolvers/useFaucetResolvers'; +import { useVestingQueryResolvers } from '../vesting/useVestingQueryResolvers'; +import { useBalanceMutationsResolvers } from '../balances/resolvers/mutation/balanceTransfer'; +import { useConfigQueryResolvers } from '../config/useConfigQueryResolvers'; /** * Add all local gql resolvers here @@ -35,18 +38,21 @@ export const useResolvers: () => Resolvers = () => { ...useBalanceQueryResolvers(), ...PoolsQueryResolver, ...useAssetsQueryResolvers(), + ...useConfigQueryResolvers(), }, Mutation: { ...AccountsMutationResolvers, ...useVestingMutationResolvers(), ...useConfigMutationResolvers(), ...usePoolsMutationResolvers(), - ...useFaucetResolvers().Mutation + ...useFaucetResolvers().Mutation, + ...useBalanceMutationsResolvers() }, XYKPool, LBPPool, Account: { - ...useBalanceQueryResolvers() + ...useBalanceQueryResolvers(), + ...useVestingQueryResolvers() } }; }; diff --git a/src/hooks/balances/graphql/TransferBalance.mutation.graphql b/src/hooks/balances/graphql/TransferBalance.mutation.graphql index 8bb3d14c..f047254f 100644 --- a/src/hooks/balances/graphql/TransferBalance.mutation.graphql +++ b/src/hooks/balances/graphql/TransferBalance.mutation.graphql @@ -1,3 +1,3 @@ -mutation TransferBalance($from: String!, $to: String!, $currencyId: String!, $amount: String) { - transferBalance(from: $from, to: $to, currencyId: $currencyId, amount: $amount) @client +mutation TransferBalance($to: String!, $currencyId: String!, $amount: String) { + transferBalance(to: $to, currencyId: $currencyId, amount: $amount) @client } \ No newline at end of file diff --git a/src/hooks/balances/resolvers/mutation/balanceTransfer.tsx b/src/hooks/balances/resolvers/mutation/balanceTransfer.tsx index e0ee7b81..c03d2619 100644 --- a/src/hooks/balances/resolvers/mutation/balanceTransfer.tsx +++ b/src/hooks/balances/resolvers/mutation/balanceTransfer.tsx @@ -1,20 +1,24 @@ import { ApiPromise } from '@polkadot/api'; import { usePolkadotJsContext } from '../../../polkadotJs/usePolkadotJs'; import errors from '../../../../errors'; -import { useMutation } from '@apollo/client'; +import { ApolloCache, useMutation } from '@apollo/client'; import { loader } from 'graphql.macro'; import { withGracefulErrors, gracefulExtensionCancelationErrorHandler as gracefulExtensionCancellationErrorHandler, + vestingClaimHandler, } from '../../../vesting/useVestingMutationResolvers'; import { web3FromAddress } from '@polkadot/extension-dapp'; import { DispatchError, ExtrinsicStatus } from '@polkadot/types/interfaces'; import log from 'loglevel'; import { withErrorHandler } from '../../../apollo/withErrorHandler'; import { useMemo } from 'react'; +import { readActiveAccount } from '../../../accounts/lib/readActiveAccount'; +import { add } from 'lodash'; +import { xykBuyHandler } from '../../../pools/xyk/buy'; export const TRANSFER_BALANCE = loader( - './graphql/TransferBalance.mutation.graphql' + './../../graphql/TransferBalance.mutation.graphql' ); export interface TransferBalanceMutationVariables { @@ -36,63 +40,61 @@ export type reject = (error?: any) => void; // TODO: use handler from #71 export const transferBalanceHandler = - (apiInstance: ApiPromise, resolve: resolve, reject: reject) => - ({ - status, - dispatchError, - }: { - status: ExtrinsicStatus; - dispatchError?: DispatchError; - }) => { - if (status.isFinalized) log.info('operation finalized'); + (apiInstance: ApiPromise, resolve: resolve, reject: reject) => { + return vestingClaimHandler(resolve, reject, apiInstance); + } - // TODO: handle status via the action log / notification stack - if (status.isInBlock) { - if (dispatchError?.isModule) { - return log.error( - 'transfer unsuccessful', - apiInstance.registry.findMetaError(dispatchError.asModule) - ); - } +const transferBalanceExtrinsic = (apiInstance: ApiPromise) => + apiInstance.tx.currencies.transfer; - return log.info('transfer successful'); - } +export const estimateBalanceTransfer = async ( + cache: ApolloCache, + apiInstance: ApiPromise, + args: TransferBalanceMutationVariables +) => { + const activeAccount = readActiveAccount(cache); + const address = activeAccount?.id; - // if the operation has been broadcast, finish the mutation - if (status.isBroadcast) { - log.info('transaction has been broadcast'); - return resolve(); - } - if (dispatchError) { - log.error( - 'There was a dispatch error', - apiInstance.registry.findMetaError(dispatchError.asModule) - ); - return reject(); - } - }; + if (!address) + throw new Error(`Can't retrieve sender's address for estimation`); + if (!args.from || !args.to || !args.currencyId || !args.amount) + throw new Error(errors.invalidTransferVariables); + + return transferBalanceExtrinsic(apiInstance) + .apply(apiInstance, [args.to, args.currencyId, args.amount]) + .paymentInfo(address); +}; const balanceTransferMutationResolverFactory = (apiInstance?: ApiPromise) => - async (_obj: any, args: TransferBalanceMutationVariables) => { - if (!args.from || !args.to || !args.currencyId || !args.amount) + async (_obj: any, args: TransferBalanceMutationVariables, { cache }: { cache: ApolloCache }) => { + if (!args.to || !args.currencyId || !args.amount) throw new Error(errors.invalidTransferVariables); if (!apiInstance) throw new Error(errors.apiInstanceNotInitialized); - return withGracefulErrors( - async (resolve, reject) => { - const { signer } = await web3FromAddress(args.from!); + // return withGracefulErrors( + // , + // [gracefulExtensionCancellationErrorHandler] + // ); + + await new Promise(async (resolve, reject) => { + try { + const activeAccount = readActiveAccount(cache); + const address = activeAccount?.id; + if (!address) return reject(new Error('No active account found!')); + const { signer } = await web3FromAddress(address); - await apiInstance.tx.currencies.transfer + await transferBalanceExtrinsic(apiInstance) .apply(apiInstance, [args.to, args.currencyId, args.amount]) .signAndSend( - args.from!, + address, { signer }, - transferBalanceHandler(apiInstance, resolve, reject) + xykBuyHandler(resolve, reject, apiInstance) ); - }, - [gracefulExtensionCancellationErrorHandler] - ); + } catch (e) { + reject(e) + } + }) }; export const useBalanceMutationsResolvers = () => { diff --git a/src/hooks/balances/resolvers/useBalanceMutationResolvers.tsx b/src/hooks/balances/resolvers/useBalanceMutationResolvers.tsx index 0017bc3b..eeec46ac 100644 --- a/src/hooks/balances/resolvers/useBalanceMutationResolvers.tsx +++ b/src/hooks/balances/resolvers/useBalanceMutationResolvers.tsx @@ -54,7 +54,7 @@ export const balanceMutationResolverFactory = (apiInstance?: ApiPromise) => async (_obj: any, args: TransferBalanceMutationVariables) => { if (!apiInstance) throw Error(errors.apiInstanceNotInitialized); - if (!args.from || !args.to || !args.currencyId || !args.amount) + if (!args.to || !args.currencyId || !args.amount) throw new Error(errors.invalidTransferVariables); return withGracefulErrors( diff --git a/src/hooks/balances/resolvers/useTransferMutation.tsx b/src/hooks/balances/resolvers/useTransferMutation.tsx index 45221a7d..49abe490 100644 --- a/src/hooks/balances/resolvers/useTransferMutation.tsx +++ b/src/hooks/balances/resolvers/useTransferMutation.tsx @@ -1,20 +1,15 @@ -import { useMutation } from '@apollo/client'; +import { MutationHookOptions, useMutation } from '@apollo/client'; import { loader } from 'graphql.macro'; export const TRANSFER_BALANCE = loader( - './graphql/TransferBalance.mutation.graphql' + './../graphql/TransferBalance.mutation.graphql' ); export interface TransferBalanceMutationVariables { - from?: string; to?: string; currencyId?: string; amount?: string; } -export const useTransferBalanceMutation = ( - variables: TransferBalanceMutationVariables -) => - useMutation(TRANSFER_BALANCE, { - variables, - }); +export const useTransferBalanceMutation = (options?: MutationHookOptions) => + useMutation(TRANSFER_BALANCE, options); diff --git a/src/hooks/config/useConfigMutationResolver.tsx b/src/hooks/config/useConfigMutationResolver.tsx index 1ca90f70..2bf8fb10 100644 --- a/src/hooks/config/useConfigMutationResolver.tsx +++ b/src/hooks/config/useConfigMutationResolver.tsx @@ -16,6 +16,7 @@ import { } from '../vesting/useVestingMutationResolvers'; import { defaultConfigValue, usePersistentConfig } from './usePersistentConfig'; import { SetConfigMutationVariables } from './useSetConfigMutation'; +import { xykBuyHandler } from '../pools/xyk/buy'; export const defaultAssetId = '0'; @@ -38,13 +39,14 @@ export const useConfigMutationResolvers = () => { if (!apiInstance || loading) return; // TODO: return an optimistic update to the cache with the new config - await withGracefulErrors( - async (resolve, reject) => { - const address = cache.readQuery({ - query: GET_ACTIVE_ACCOUNT, - })?.activeAccount?.id; + // await withGracefulErrors( + await new Promise(async (resolve, reject) => { + const address = cache.readQuery({ + query: GET_ACTIVE_ACCOUNT, + })?.activeAccount?.id; - if (!address) return resolve(); + try { + if (!address) return reject(); const { signer } = await web3FromAddress(address); @@ -53,18 +55,22 @@ export const useConfigMutationResolvers = () => { .signAndSend( address, { signer }, - setCurrencyHandler(resolve, reject) + xykBuyHandler(resolve, reject, apiInstance) ); - }, - [gracefulExtensionCancelationErrorHandler] - ); + } catch (e) { + reject(e) + } + }) + // [gracefulExtensionCancelationErrorHandler] + // [] + // ); const persistableConfig = args.config; // there's no point in persisting the feePaymentAsset since it will // be refetched from the node anyways delete persistableConfig?.feePaymentAsset; - setPersistedConfig(persistableConfig || defaultConfigValue); + // setPersistedConfig(persistableConfig || defaultConfigValue); }, [apiInstance, loading, setPersistedConfig] ) diff --git a/src/hooks/config/useConfigQueryResolvers.tsx b/src/hooks/config/useConfigQueryResolvers.tsx index 514000aa..a0f89f11 100644 --- a/src/hooks/config/useConfigQueryResolvers.tsx +++ b/src/hooks/config/useConfigQueryResolvers.tsx @@ -26,6 +26,7 @@ export const useConfigQueryResolvers = () => { _variables, { cache }: { cache: ApolloCache } ) => { + console.log('config query resolver') if (!apiInstance || loading) return; // TODO: evict config from the cache after active account changes @@ -33,6 +34,8 @@ export const useConfigQueryResolvers = () => { query: GET_ACTIVE_ACCOUNT, })?.activeAccount?.id; + if (!address) return; + let feePaymentAsset = address ? apiInstance .createType( @@ -44,6 +47,8 @@ export const useConfigQueryResolvers = () => { ?.toHuman() : null; + console.log('found fee payment asset', feePaymentAsset); + feePaymentAsset = feePaymentAsset ? feePaymentAsset : nativeAssetId; return { diff --git a/src/hooks/config/useGetConfigQuery.tsx b/src/hooks/config/useGetConfigQuery.tsx index f15f6430..de126c08 100644 --- a/src/hooks/config/useGetConfigQuery.tsx +++ b/src/hooks/config/useGetConfigQuery.tsx @@ -1,4 +1,4 @@ -import { useQuery } from '@apollo/client'; +import { QueryHookOptions, useQuery } from '@apollo/client'; import { loader } from 'graphql.macro'; import { Query } from '../../generated/graphql'; @@ -8,6 +8,7 @@ export interface GetConfigQueryResponse { config: Query['config'] } -export const useGetConfigQuery = () => useQuery(GET_CONFIG, { - notifyOnNetworkStatusChange: true +export const useGetConfigQuery = (options?: QueryHookOptions) => useQuery(GET_CONFIG, { + notifyOnNetworkStatusChange: true, + ...options }); \ No newline at end of file diff --git a/src/hooks/config/useSetConfigMutation.tsx b/src/hooks/config/useSetConfigMutation.tsx index c265bfb3..c54298ee 100644 --- a/src/hooks/config/useSetConfigMutation.tsx +++ b/src/hooks/config/useSetConfigMutation.tsx @@ -6,7 +6,7 @@ import { GET_CONFIG } from './useGetConfigQuery'; export const SET_CONFIG = loader('./graphql/SetConfig.mutation.graphql'); export interface SetConfigMutationVariables { - config: Config | undefined + config: Partial | undefined } export const useSetConfigMutation = (onCompleted?: () => void) => useMutation(SET_CONFIG, { diff --git a/src/hooks/extension/lib/getExtension.tsx b/src/hooks/extension/lib/getExtension.tsx index 0585f9ca..d74abbbb 100644 --- a/src/hooks/extension/lib/getExtension.tsx +++ b/src/hooks/extension/lib/getExtension.tsx @@ -12,9 +12,7 @@ export const getExtension = async (): Promise => { const { isAvailable }: Pick = await new Promise( (resolve, reject) => { promiseRetry(async (retry, attempt) => { - console.log('attempt', attempt); const isAvailable = !!(window as any).injectedWeb3?.['polkadot-js']; - console.log('getExtension attempt: #', attempt, isAvailable); isAvailable ? ( resolve({ diff --git a/src/hooks/extension/queries/useGetExtensionQuery.tsx b/src/hooks/extension/queries/useGetExtensionQuery.tsx index 462cec50..69f35f9b 100644 --- a/src/hooks/extension/queries/useGetExtensionQuery.tsx +++ b/src/hooks/extension/queries/useGetExtensionQuery.tsx @@ -1,4 +1,5 @@ import { QueryHookOptions, useQuery } from '@apollo/client'; +import constate from 'constate'; import { loader } from 'graphql.macro'; import { Extension } from '../../../generated/graphql'; @@ -11,8 +12,9 @@ export interface GetExtensionQueryResponse { } // hook wrapping the built-in apollo useQuery hook with proper types & configuration -export const useGetExtensionQuery = (options?: QueryHookOptions) => +export const useGetExtensionQuery = () => useQuery(GET_EXTENSION, { notifyOnNetworkStatusChange: true, - ...options, }); + +export const [GetExtensionQueryProvider, useGetExtensionQueryContext] = constate(useGetExtensionQuery) diff --git a/src/hooks/math/useMath.tsx b/src/hooks/math/useMath.tsx index 2c1ea61e..7d78b790 100644 --- a/src/hooks/math/useMath.tsx +++ b/src/hooks/math/useMath.tsx @@ -6,6 +6,10 @@ export interface HydraDxMathXyk { get_spot_price: (a: string, b: string, c: string) => string | undefined, calculate_in_given_out: (a: string, b: string, c: string) => string | undefined, calculate_out_given_in: (a: string, b: string, c: string) => string | undefined + calculate_liquidity_in: (a: string, b: string, c: string) => string | undefined, + calculate_liquidity_out_asset_a: (a: string, b: string, c: string, d: string) => string | undefined, + calculate_liquidity_out_asset_b: (a: string, b: string, c: string, d: string) => string | undefined, + calculate_shares: (a: string, b: string, c: string) => string | undefined; } export interface HydraDxMathLbp { diff --git a/src/hooks/misc/useLoading.tsx b/src/hooks/misc/useLoading.tsx index 71cd5bbf..b61b55e8 100644 --- a/src/hooks/misc/useLoading.tsx +++ b/src/hooks/misc/useLoading.tsx @@ -1,8 +1,20 @@ +import { useEffect, useState } from 'react'; +import { useLastBlockContext } from '../lastBlock/useSubscribeNewBlockNumber'; import { useMathContext } from '../math/useMath'; import { usePolkadotJsContext } from '../polkadotJs/usePolkadotJs'; export const useLoading = () => { - const { loading } = usePolkadotJsContext(); + const { loading: polkadotJsLoading } = usePolkadotJsContext(); const { math } = useMathContext(); - return loading || !math; + const lastBlock = useLastBlockContext() + + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!polkadotJsLoading && math && lastBlock) { + setLoading(false); + } + }, [polkadotJsLoading, math, lastBlock]); + + return loading; }; diff --git a/src/hooks/polkadotJs/signAndSend.test.tsx b/src/hooks/polkadotJs/signAndSend.test.tsx new file mode 100644 index 00000000..360248cc --- /dev/null +++ b/src/hooks/polkadotJs/signAndSend.test.tsx @@ -0,0 +1,127 @@ +import { ApiPromise } from '@polkadot/api'; +import { signAndSend } from './signAndSend'; +import { web3FromAddress } from '@polkadot/extension-dapp'; +import { InMemoryCache } from '@apollo/client'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { ISubmittableResult } from '@polkadot/types/types'; +import { readActiveAccount } from '../accounts/lib/readActiveAccount'; + +const web3FromAddressMocked = web3FromAddress as jest.Mock; +const readActiveAccountMocked = readActiveAccount as jest.Mock; + +jest.mock('@polkadot/extension-dapp', () => { + return { web3FromAddress: jest.fn() }; +}); +jest.mock('../accounts/readActiveAccount', () => { + return { readActiveAccount: jest.fn() }; +}); + +const extrinsicFailedIsMock = jest.fn(); +const findMetaErrorMock = jest.fn(); + +export const getMockApiPromise = (): jest.Mocked => + ({ + events: { system: { ExtrinsicFailed: { is: extrinsicFailedIsMock } } }, + registry: { findMetaError: findMetaErrorMock }, + } as unknown as jest.Mocked); + +describe('signAndSend', () => { + let mockApiInstance: jest.Mocked; + let apolloCache = new InMemoryCache(); + let transactionSignAndSendMock = jest.fn(); + let transaction = { + signAndSend: transactionSignAndSendMock, + } as unknown as SubmittableExtrinsic<'promise', ISubmittableResult>; + let signer = {}; + let unsubscribe = jest.fn(); + let address = { + id: 'address-id', + }; + + const setupTransactionSignAndSendMock = (data: object) => { + transactionSignAndSendMock.mockImplementation( + async (_addressId, _signer, callback) => { + setTimeout(() => callback(data), 0); + + return unsubscribe; + } + ); + }; + + beforeEach(() => { + jest.resetAllMocks(); + mockApiInstance = getMockApiPromise(); + findMetaErrorMock.mockImplementation((arg) => arg); + web3FromAddressMocked.mockResolvedValue({ signer }); + readActiveAccountMocked.mockReturnValue(address); + }); + + it('throws error if no active account is selected', async () => { + readActiveAccountMocked.mockReturnValue(null); + + await expect( + signAndSend(apolloCache, transaction, mockApiInstance) + ).rejects.toThrow(); + }); + + it('resolves if there are no errors in signAndSend', async () => { + setupTransactionSignAndSendMock({ + status: { isInBlock: true }, + events: [], + }); + + await expect( + signAndSend(apolloCache, transaction, mockApiInstance) + ).resolves.toBeNull(); + expect(web3FromAddressMocked).toBeCalledTimes(1); + expect(web3FromAddressMocked).toBeCalledWith(address.id); + expect(unsubscribe).toBeCalledTimes(1); + }); + + describe('extrinsic errors', () => { + beforeEach(() => { + extrinsicFailedIsMock.mockReturnValue(true); + }); + + it('rejects with catalog meta error', async () => { + const mockedError = { + error: 'mocked-error', + isModule: true, + asModule: { + docs: ['mocked', 'docs'], + method: 'mocked-method', + section: 'mocked-section', + }, + }; + setupTransactionSignAndSendMock({ + status: { isInBlock: true }, + events: [{ event: { data: [mockedError] } }], + }); + + await expect( + signAndSend(apolloCache, transaction, mockApiInstance) + ).rejects.toEqual({ + errors: expect.arrayContaining([mockedError.asModule]), + }); + expect(unsubscribe).toBeCalledTimes(1); + }); + + it('rejects with other error', async () => { + const mockedError = { + error: 'mocked-error', + isModule: false, + }; + setupTransactionSignAndSendMock({ + status: { isInBlock: true }, + events: [{ event: { data: [mockedError] } }], + }); + + await expect( + signAndSend(apolloCache, transaction, mockApiInstance) + ).rejects.toEqual({ + errors: expect.arrayContaining([mockedError]), + }); + expect(unsubscribe).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/hooks/polkadotJs/signAndSend.tsx b/src/hooks/polkadotJs/signAndSend.tsx new file mode 100644 index 00000000..2f13bda9 --- /dev/null +++ b/src/hooks/polkadotJs/signAndSend.tsx @@ -0,0 +1,76 @@ +import { ApolloCache } from '@apollo/client'; +import { ApiPromise } from '@polkadot/api'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { web3FromAddress } from '@polkadot/extension-dapp'; +import { DispatchError, EventRecord } from '@polkadot/types/interfaces'; +import { + Callback, + ISubmittableResult, + RegistryError, +} from '@polkadot/types/types'; +import { readActiveAccount } from '../accounts/lib/readActiveAccount'; + +export type ExtrinsicErrors = RegistryError | DispatchError; + +export const parseExtrinsicErrors = ( + events: EventRecord[], + apiInstance: ApiPromise +): ExtrinsicErrors[] => + events + .filter(({ event }) => apiInstance.events.system.ExtrinsicFailed.is(event)) + // we know that data for system.ExtrinsicFailed is + // (DispatchError, DispatchInfo) + .reduce((acc: ExtrinsicErrors[], { event: { data } }) => { + const error: DispatchError = data[0] as DispatchError; + if (error.isModule) { + // for module errors, we have the section indexed, lookup + const decoded = apiInstance.registry.findMetaError(error.asModule); + acc.push(decoded); + } else { + // Other, CannotLookup, BadOrigin, no extra info + acc.push(error); + } + + return acc; + }, []); + +export const signAndSend = async ( + cache: ApolloCache, + transaction: SubmittableExtrinsic<'promise', ISubmittableResult>, + apiInstance: ApiPromise +) => { + const address = readActiveAccount(cache); + // if for some reason the UI tries to send a transaction, and there is no active account selected + if (!address) { + throw new Error('No active account found'); + } + const { signer } = await web3FromAddress(address.id); + + return new Promise(async (resolve, reject) => { + const statusHandler: Callback = ({ + status, + events, + }) => { + if (!status.isInBlock) { + return; + } + const errors = parseExtrinsicErrors(events, apiInstance); + + if (errors.length > 0) { + reject({ errors }); + } else { + resolve(null); + } + + if (unsub) { + unsub(); + } + }; + + const unsub = await transaction.signAndSend( + address.id, + { signer }, + statusHandler + ); + }); +}; diff --git a/src/hooks/polkadotJs/usePolkadotJs.tsx b/src/hooks/polkadotJs/usePolkadotJs.tsx index 801866d0..4269813b 100644 --- a/src/hooks/polkadotJs/usePolkadotJs.tsx +++ b/src/hooks/polkadotJs/usePolkadotJs.tsx @@ -25,7 +25,7 @@ const getPoolAccount = { ], type: 'AccountId', }; -const rpc = { +export const rpc = { xyk: { getPoolAccount, }, @@ -34,12 +34,12 @@ const rpc = { }, }; -const types = { +export const types = { ...typesConfig.types[0], ...ormlTypes, }; -const typesAlias = { +export const typesAlias = { ...typesConfig.alias, ...ormlTypesAlias, }; diff --git a/src/hooks/pools/graphql/AddLiquidity.mutation.graphql b/src/hooks/pools/graphql/AddLiquidity.mutation.graphql new file mode 100644 index 00000000..2c481705 --- /dev/null +++ b/src/hooks/pools/graphql/AddLiquidity.mutation.graphql @@ -0,0 +1,13 @@ +mutation AddLiquidity( + $assetA: String!, + $assetB: String!, + $amountA: String!, + $amountBMaxLimit: String! +) { + addLiquidity( + assetA: $assetA, + assetB: $assetB, + amountA: $amountA, + amountBMaxLimit: $amountBMaxLimit + ) @client +} \ No newline at end of file diff --git a/src/hooks/pools/graphql/GetPoolByAssets.query.graphql b/src/hooks/pools/graphql/GetPoolByAssets.query.graphql index 5fd5b6c7..20df8aa6 100644 --- a/src/hooks/pools/graphql/GetPoolByAssets.query.graphql +++ b/src/hooks/pools/graphql/GetPoolByAssets.query.graphql @@ -12,6 +12,8 @@ query GetPoolByAssets($assetInId: String!, $assetOutId: String!) { assetId, balance }, + shareTokenId, + totalLiquidity # TODO: investigate how caching works when these fields are missing for XYK pools # lbp fields, diff --git a/src/hooks/pools/graphql/Pool.graphql b/src/hooks/pools/graphql/Pool.graphql index 71e7cb97..89f5f1bf 100644 --- a/src/hooks/pools/graphql/Pool.graphql +++ b/src/hooks/pools/graphql/Pool.graphql @@ -40,9 +40,11 @@ type XYKPool { assetInId: String! assetOutId: String! balances: [Balance!] + totalLiquidity: String!, + shareTokenId: String! } -union Pool = LBPPool | XYKPool +union Pool = XYKPool | LBPPool extend type Query { pools: [Pool!]! diff --git a/src/hooks/pools/graphql/RemoveLiquidity.mutation.graphql b/src/hooks/pools/graphql/RemoveLiquidity.mutation.graphql new file mode 100644 index 00000000..26ff0de9 --- /dev/null +++ b/src/hooks/pools/graphql/RemoveLiquidity.mutation.graphql @@ -0,0 +1,11 @@ +mutation RemoveLiquidity( + $assetA: String!, + $assetB: String!, + $amount: String! +) { + removeLiquidity( + assetA: $assetA, + assetB: $assetB, + amount: $amount + ) @client +} \ No newline at end of file diff --git a/src/hooks/pools/lbp/calculateInGivenOut.tsx b/src/hooks/pools/lbp/calculateInGivenOut.tsx index 43159f0c..027846d4 100644 --- a/src/hooks/pools/lbp/calculateInGivenOut.tsx +++ b/src/hooks/pools/lbp/calculateInGivenOut.tsx @@ -1,27 +1,28 @@ import { find } from 'lodash'; -import { LbpPool, Pool } from '../../../generated/graphql'; +// import { LbpPool, Pool } from '../../../generated/graphql'; import { HydraDxMath } from '../../math/useMath'; +import { Pool } from '../../../generated/graphql'; -/** - * Wrapper for `math.lbp.calculate_in_given_out` - * @param math - * @param inReserve - * @param outReserve - * @param inWeight - * @param outWeight - * @param amount - * @returns - */ -export const calculateInGivenOut = ( - math: HydraDxMath, - inReserve: string, - outReserve: string, - inWeight: string, - outWeight: string, - amount: string -) => { - return math.lbp.calculate_in_given_out(inReserve, outReserve, inWeight, outWeight, amount); -} +// /** +// * Wrapper for `math.lbp.calculate_in_given_out` +// * @param math +// * @param inReserve +// * @param outReserve +// * @param inWeight +// * @param outWeight +// * @param amount +// * @returns +// */ +// export const calculateInGivenOut = ( +// math: HydraDxMath, +// inReserve: string, +// outReserve: string, +// inWeight: string, +// outWeight: string, +// amount: string +// ) => { +// return math.lbp.calculate_in_given_out(inReserve, outReserve, inWeight, outWeight, amount); +// } export const getPoolBalances = (pool: Pool, assetInId: string, assetOutId: string) => { const assetABalance = find(pool.balances, { assetId: assetInId })?.balance; @@ -30,41 +31,41 @@ export const getPoolBalances = (pool: Pool, assetInId: string, assetOutId: strin return { assetABalance, assetBBalance } } -export const getInAndOutWeights = (pool: LbpPool, assetInId: string, assetOutId: string) => { - const assetInWeight = assetInId === pool.assetInId - ? pool.assetAWeights.current - : pool.assetBWeights.current +// export const getInAndOutWeights = (pool: LbpPool, assetInId: string, assetOutId: string) => { +// const assetInWeight = assetInId === pool.assetInId +// ? pool.assetAWeights.current +// : pool.assetBWeights.current - const assetOutWeight = assetOutId === pool.assetOutId - ? pool.assetBWeights.current - : pool.assetAWeights.current; +// const assetOutWeight = assetOutId === pool.assetOutId +// ? pool.assetBWeights.current +// : pool.assetAWeights.current; - return { assetInWeight, assetOutWeight }; -} +// return { assetInWeight, assetOutWeight }; +// } -export const calculateInGivenOutFromPool = ( - math: HydraDxMath, - pool: LbpPool, - assetInId: string, - assetOutId: string, - amountOut: string, -) => { - const { assetABalance: assetInBalance, assetBBalance: assetOutBalance } = getPoolBalances( - pool, - assetInId, - assetOutId, - ) +// export const calculateInGivenOutFromPool = ( +// math: HydraDxMath, +// pool: LbpPool, +// assetInId: string, +// assetOutId: string, +// amountOut: string, +// ) => { +// const { assetABalance: assetInBalance, assetBBalance: assetOutBalance } = getPoolBalances( +// pool, +// assetInId, +// assetOutId, +// ) - if (!assetInBalance || !assetOutBalance) throw new Error(`Can't find the required balances in the pool`); +// if (!assetInBalance || !assetOutBalance) throw new Error(`Can't find the required balances in the pool`); - const { assetInWeight, assetOutWeight } = getInAndOutWeights(pool, assetInId, assetOutId); +// const { assetInWeight, assetOutWeight } = getInAndOutWeights(pool, assetInId, assetOutId); - return calculateInGivenOut( - math, - assetInBalance, - assetOutBalance, - assetInWeight, - assetOutWeight, - amountOut - ); -} \ No newline at end of file +// return calculateInGivenOut( +// math, +// assetInBalance, +// assetOutBalance, +// assetInWeight, +// assetOutWeight, +// amountOut +// ); +// } \ No newline at end of file diff --git a/src/hooks/pools/lbp/calculateOutGivenIn.tsx b/src/hooks/pools/lbp/calculateOutGivenIn.tsx index 9a1c0dc7..a16f5d05 100644 --- a/src/hooks/pools/lbp/calculateOutGivenIn.tsx +++ b/src/hooks/pools/lbp/calculateOutGivenIn.tsx @@ -1,52 +1,54 @@ -import { find } from 'lodash'; -import { LbpPool } from '../../../generated/graphql'; -import { HydraDxMath } from '../../math/useMath'; -import { getInAndOutWeights, getPoolBalances } from './calculateInGivenOut'; +// import { find } from 'lodash'; +// import { LbpPool } from '../../../generated/graphql'; +// import { HydraDxMath } from '../../math/useMath'; +// import { getInAndOutWeights, getPoolBalances } from './calculateInGivenOut'; -/** - * Wrapper for `math.lbp.calculate_out_given_in` - * @param math - * @param inReserve - * @param outReserve - * @param inWeight - * @param outWeight - * @param amount - * @returns - */ -export const calculateOutGivenIn = ( - math: HydraDxMath, - inReserve: string, - outReserve: string, - inWeight: string, - outWeight: string, - amount: string, -) => { - return math.lbp.calculate_out_given_in(inReserve, outReserve, inWeight, outWeight, amount); -} +// /** +// * Wrapper for `math.lbp.calculate_out_given_in` +// * @param math +// * @param inReserve +// * @param outReserve +// * @param inWeight +// * @param outWeight +// * @param amount +// * @returns +// */ +// export const calculateOutGivenIn = ( +// math: HydraDxMath, +// inReserve: string, +// outReserve: string, +// inWeight: string, +// outWeight: string, +// amount: string, +// ) => { +// return math.lbp.calculate_out_given_in(inReserve, outReserve, inWeight, outWeight, amount); +// } -export const calculateOutGivenInFromPool = ( - math: HydraDxMath, - pool: LbpPool, - assetInId: string, - assetOutId: string, - amountIn: string, -) => { - const { assetABalance: assetInBalance, assetBBalance: assetOutBalance } = getPoolBalances( - pool, - assetInId, - assetOutId, - ) +// export const calculateOutGivenInFromPool = ( +// math: HydraDxMath, +// pool: LbpPool, +// assetInId: string, +// assetOutId: string, +// amountIn: string, +// ) => { +// const { assetABalance: assetInBalance, assetBBalance: assetOutBalance } = getPoolBalances( +// pool, +// assetInId, +// assetOutId, +// ) - if (!assetInBalance || !assetOutBalance) throw new Error(`Can't find the required balances in the pool`); +// if (!assetInBalance || !assetOutBalance) throw new Error(`Can't find the required balances in the pool`); - const { assetInWeight, assetOutWeight } = getInAndOutWeights(pool, assetInId, assetOutId); +// const { assetInWeight, assetOutWeight } = getInAndOutWeights(pool, assetInId, assetOutId); - return calculateOutGivenIn( - math, - assetInBalance, - assetOutBalance, - assetInWeight, - assetOutWeight, - amountIn - ); -} \ No newline at end of file +// return calculateOutGivenIn( +// math, +// assetInBalance, +// assetOutBalance, +// assetInWeight, +// assetOutWeight, +// amountIn +// ); +// } + +export default {}; \ No newline at end of file diff --git a/src/hooks/pools/mutations/useAddLiquidityMutation.tsx b/src/hooks/pools/mutations/useAddLiquidityMutation.tsx new file mode 100644 index 00000000..16cf5819 --- /dev/null +++ b/src/hooks/pools/mutations/useAddLiquidityMutation.tsx @@ -0,0 +1,23 @@ +import { MutationHookOptions, useMutation } from '@apollo/client'; +import { loader } from 'graphql.macro'; +import { PoolType } from '../../../components/Chart/shared'; +import { TradeType } from '../../../generated/graphql'; + +const REMOVE_LIQUIDITY = loader('./../graphql/AddLiquidity.mutation.graphql'); + +export interface AddLiquidityMutationVariables { + assetA: string; + assetB: string; + amountA: string; + amountBMaxLimit: string; +} + +export const useAddLiquidityMutation = ( + options?: MutationHookOptions +) => + useMutation(REMOVE_LIQUIDITY, { + notifyOnNetworkStatusChange: true, + ...options, + }); + + \ No newline at end of file diff --git a/src/hooks/pools/mutations/useRemoveLiquidityMutation.tsx b/src/hooks/pools/mutations/useRemoveLiquidityMutation.tsx new file mode 100644 index 00000000..631c662f --- /dev/null +++ b/src/hooks/pools/mutations/useRemoveLiquidityMutation.tsx @@ -0,0 +1,22 @@ +import { MutationHookOptions, useMutation } from '@apollo/client'; +import { loader } from 'graphql.macro'; +import { PoolType } from '../../../components/Chart/shared'; +import { TradeType } from '../../../generated/graphql'; + +const REMOVE_LIQUIDITY = loader('./../graphql/RemoveLiquidity.mutation.graphql'); + +export interface RemoveLiquidityMutationVariables { + assetA: string; + assetB: string; + amount: string +} + +export const useRemoveLiquidityMutation = ( + options?: MutationHookOptions +) => + useMutation(REMOVE_LIQUIDITY, { + notifyOnNetworkStatusChange: true, + ...options, + }); + + \ No newline at end of file diff --git a/src/hooks/pools/mutations/useSubmitTradeMutation.tsx b/src/hooks/pools/mutations/useSubmitTradeMutation.tsx index 09a35030..fe151edf 100644 --- a/src/hooks/pools/mutations/useSubmitTradeMutation.tsx +++ b/src/hooks/pools/mutations/useSubmitTradeMutation.tsx @@ -6,28 +6,27 @@ import { TradeType } from '../../../generated/graphql'; const SUBMIT_TRADE = loader('./../graphql/SubmitTrade.mutation.graphql'); export interface SubmitTradeMutationVariables { - assetInId: string, - assetOutId: string, - assetInAmount: string, - assetOutAmount: string, - poolType: PoolType, - tradeType: TradeType, - amountWithSlippage: string + assetInId: string; + assetOutId: string; + assetInAmount: string; + assetOutAmount: string; + poolType: PoolType; + tradeType: TradeType; + amountWithSlippage: string; } -export const useSubmitTradeMutation = (options?: MutationHookOptions) => useMutation( - SUBMIT_TRADE, - { - notifyOnNetworkStatusChange: true, - ...options - } -) +export const useSubmitTradeMutation = ( + options?: MutationHookOptions +) => + useMutation(SUBMIT_TRADE, { + notifyOnNetworkStatusChange: true, + ...options, + }); /** * lbp.buy(assetOut, assetIn, amount, maxLimit) * lbp.sell(assetIn, assetOut, amount, maxLimit) - * + * * exchange.buy(assetBuy, assetSell, amountBuy, maxSold, discount) * exchange.sell(assetSell, assetBuy, amountSell, minBought, discount) */ - diff --git a/src/hooks/pools/resolvers/useAddLiquidityMutationResolver.tsx b/src/hooks/pools/resolvers/useAddLiquidityMutationResolver.tsx new file mode 100644 index 00000000..2f74f915 --- /dev/null +++ b/src/hooks/pools/resolvers/useAddLiquidityMutationResolver.tsx @@ -0,0 +1,47 @@ +import { ApolloCache, NormalizedCacheObject } from "@apollo/client"; +import { web3FromAddress } from "@polkadot/extension-dapp"; +import { Maybe } from "graphql/jsutils/Maybe"; +import { useCallback } from "react"; +import { readActiveAccount } from "../../accounts/lib/readActiveAccount"; +import { usePolkadotJsContext } from "../../polkadotJs/usePolkadotJs"; +import { AddLiquidityMutationVariables } from "../mutations/useAddLiquidityMutation"; +import { RemoveLiquidityMutationVariables } from "../mutations/useRemoveLiquidityMutation"; +import { SubmitTradeMutationVariables } from "../mutations/useSubmitTradeMutation"; +import { xykBuyHandler } from "../xyk/buy"; + +export const useAddLiquidityMutationResolver = () => { + const { apiInstance } = usePolkadotJsContext(); + + // return withErrorHandler( + return useCallback( + async ( + _obj, + args: Maybe, + { cache }: { cache: ApolloCache } + ) => { + + await new Promise(async (resolve, reject) => { + const activeAccount = readActiveAccount(cache); + const address = activeAccount?.id; + + // TODO: extract this error + try { + if (!address) return reject(new Error('No active account found!')); + + const { signer } = await web3FromAddress(address); + + await apiInstance?.tx.xyk.addLiquidity(args?.assetA, args?.assetB, args?.amountA, args?.amountBMaxLimit) + .signAndSend( + address, + { signer }, + xykBuyHandler(resolve, reject, apiInstance) + ); + } catch (e) { + reject(e) + } + }) + }, + [apiInstance] + ) + // ); +}; \ No newline at end of file diff --git a/src/hooks/pools/resolvers/usePoolsMutationResolvers.tsx b/src/hooks/pools/resolvers/usePoolsMutationResolvers.tsx index fd88616b..06a09740 100644 --- a/src/hooks/pools/resolvers/usePoolsMutationResolvers.tsx +++ b/src/hooks/pools/resolvers/usePoolsMutationResolvers.tsx @@ -1,9 +1,11 @@ +import { useAddLiquidityMutationResolver } from './useAddLiquidityMutationResolver'; +import { useRemoveLiquidityMutationResolver } from './useRemoveLiquidityMutationResolver'; import { useSubmitTradeMutationResolver } from './useSubmitTradeMutationResolvers' export const usePoolsMutationResolvers = () => { - const submitTrade = useSubmitTradeMutationResolver(); - return { - submitTrade + submitTrade: useSubmitTradeMutationResolver(), + removeLiquidity: useRemoveLiquidityMutationResolver(), + addLiquidity: useAddLiquidityMutationResolver() } } \ No newline at end of file diff --git a/src/hooks/pools/resolvers/useRemoveLiquidityMutationResolver.tsx b/src/hooks/pools/resolvers/useRemoveLiquidityMutationResolver.tsx new file mode 100644 index 00000000..260a8064 --- /dev/null +++ b/src/hooks/pools/resolvers/useRemoveLiquidityMutationResolver.tsx @@ -0,0 +1,46 @@ +import { ApolloCache, NormalizedCacheObject } from "@apollo/client"; +import { web3FromAddress } from "@polkadot/extension-dapp"; +import { Maybe } from "graphql/jsutils/Maybe"; +import { useCallback } from "react"; +import { readActiveAccount } from "../../accounts/lib/readActiveAccount"; +import { usePolkadotJsContext } from "../../polkadotJs/usePolkadotJs"; +import { RemoveLiquidityMutationVariables } from "../mutations/useRemoveLiquidityMutation"; +import { SubmitTradeMutationVariables } from "../mutations/useSubmitTradeMutation"; +import { xykBuyHandler } from "../xyk/buy"; + +export const useRemoveLiquidityMutationResolver = () => { + const { apiInstance } = usePolkadotJsContext(); + + // return withErrorHandler( + return useCallback( + async ( + _obj, + args: Maybe, + { cache }: { cache: ApolloCache } + ) => { + + await new Promise(async (resolve, reject) => { + const activeAccount = readActiveAccount(cache); + const address = activeAccount?.id; + + // TODO: extract this error + try { + if (!address) return reject(new Error('No active account found!')); + + const { signer } = await web3FromAddress(address); + + await apiInstance?.tx.xyk.removeLiquidity(args?.assetA, args?.assetB, args?.amount) + .signAndSend( + address, + { signer }, + xykBuyHandler(resolve, reject, apiInstance) + ); + } catch (e) { + reject(e) + } + }) + }, + [apiInstance] + ) + // ); +}; \ No newline at end of file diff --git a/src/hooks/pools/useGetXykPool.tsx b/src/hooks/pools/useGetXykPool.tsx index 6e406863..1eca58e4 100644 --- a/src/hooks/pools/useGetXykPool.tsx +++ b/src/hooks/pools/useGetXykPool.tsx @@ -10,7 +10,9 @@ export const useGetXykPool = () => { return mapToPool(apiInstance)([ poolId, - await apiInstance.query.xyk.poolAssets(poolId) + await apiInstance.query.xyk.poolAssets(poolId), + await apiInstance.query.xyk.shareToken(poolId), + await apiInstance.query.xyk.totalLiquidity(poolId) ]); }, [ apiInstance, diff --git a/src/hooks/pools/useGetXykPools.tsx b/src/hooks/pools/useGetXykPools.tsx index 19d2a0a1..228cd718 100644 --- a/src/hooks/pools/useGetXykPools.tsx +++ b/src/hooks/pools/useGetXykPools.tsx @@ -13,15 +13,19 @@ export const mapToPoolId = ([storageKey, codec]: [StorageKey, Codec]): return [id, codec]; } -export const mapToPool = (apiInstance: ApiPromise) => ([id, codec]: [string, Codec]) => { +export const mapToPool = (apiInstance: ApiPromise) => ([id, codec, shareTokenId, totalLiquidity]: [string, Codec, Codec, Codec]) => { const poolAssets = codec.toHuman() as PoolAssets; + console.log('mapToPool', id, codec.toHuman(), shareTokenId.toHuman(), totalLiquidity.toHuman()) + if (!poolAssets) return; return { id, assetInId: poolAssets[0], assetOutId: poolAssets[1], + totalLiquidity: totalLiquidity.toString(), + shareTokenId: shareTokenId.toString() } as XykPool } @@ -30,16 +34,31 @@ export const useGetXykPools = () => { return useCallback(async (poolId?: string, assetIds?: string[]) => { if (!apiInstance || loading) return []; + console.log('getting pools'); + const pools = (await apiInstance.query.xyk.poolAssets.entries()) + .map(async (data) => { + const pool = mapToPoolId(data); + + return { + id: pool[0], + data: [ + pool[1], // assets + await apiInstance.query.xyk.shareToken(poolId || pool[0]), + await apiInstance.query.xyk.totalLiquidity(poolId || pool[0]) + ] + } + }) + .map(async (data) => { + const d = await data + return mapToPool(apiInstance)([ + d.id, + d.data[0], + d.data[1], + d.data[2] + ]) + }) || [] - if (poolId) { - return [(await apiInstance.query.xyk.poolAssets(poolId))] - .map(pool => [poolId, pool] as [string, Codec]) - .map(mapToPool(apiInstance)) - } - - return (await apiInstance.query.xyk.poolAssets.entries()) - .map(mapToPoolId) - .map(mapToPool(apiInstance)) || [] + return await Promise.all(pools); }, [ apiInstance, loading diff --git a/src/hooks/pools/xyk/removeLiquidity.tsx b/src/hooks/pools/xyk/removeLiquidity.tsx new file mode 100644 index 00000000..7785b658 --- /dev/null +++ b/src/hooks/pools/xyk/removeLiquidity.tsx @@ -0,0 +1,72 @@ +import { ApolloCache, NormalizedCacheObject } from '@apollo/client'; +import { ApiPromise } from '@polkadot/api'; +import { web3FromAddress } from '@polkadot/extension-dapp'; +import { readActiveAccount } from '../../accounts/lib/readActiveAccount'; +import { + withGracefulErrors, + gracefulExtensionCancelationErrorHandler, + vestingClaimHandler, + resolve, + reject, +} from '../../vesting/useVestingMutationResolvers'; + +export const xykRemoveLiquidityHandler = ( + resolve: resolve, + reject: reject, + apiInstance: ApiPromise +) => { + return vestingClaimHandler(resolve, reject, apiInstance); +}; + +export const discount = false; + +export const estimateRemoveLiquidity = async ( + cache: ApolloCache, + apiInstance: ApiPromise, + assetA: string, + assetB: string, + amount: string, +) => { + const activeAccount = readActiveAccount(cache); + const address = activeAccount?.id; + + if (!address) return; + + return apiInstance.tx.xyk + .removeLiquidity(assetA, assetB, amount) + .paymentInfo(address); +} + +export const removeLiquidity = async ( + cache: ApolloCache, + apiInstance: ApiPromise, + assetA: string, + assetB: string, + amount: string, +) => { + // await withGracefulErrors( + // async (resolve, reject) => { + await new Promise(async (resolve, reject) => { + const activeAccount = readActiveAccount(cache); + const address = activeAccount?.id; + + // TODO: extract this error + try { + if (!address) return reject(new Error('No active account found!')); + + const { signer } = await web3FromAddress(address); + + await apiInstance.tx.xyk + .removeLiquidity(assetA, assetB, amount) + .signAndSend( + address, + { signer }, + xykRemoveLiquidityHandler(resolve, reject, apiInstance) + ); + } catch (e) { + reject(e) + } + }) + // [gracefulExtensionCancelationErrorHandler] + // ); +}; diff --git a/src/hooks/vesting/calculateClaimableAmount.test.tsx b/src/hooks/vesting/calculateClaimableAmount.test.tsx index 5dd8b7dd..0f590d23 100644 --- a/src/hooks/vesting/calculateClaimableAmount.test.tsx +++ b/src/hooks/vesting/calculateClaimableAmount.test.tsx @@ -1,47 +1,26 @@ import BigNumber from 'bignumber.js'; -import constants from '../../constants'; -import { - calculateClaimableAmount, - calculateFutureLock, - toBN, -} from './calculateClaimableAmount'; +import { calculateLock } from './calculateClaimableAmount'; describe('calculateClaimableAmount', () => { - const vestingSchedule = { - start: '10', - period: '10', - periodCount: '30', - perPeriod: '100', - }; - const currentBlock = new BigNumber(30); - const lockedTokens = { id: 'ormlvest', amount: '10000' }; + describe('calculateLock', () => { + const vestingSchedule = { + start: '10', + period: '10', + periodCount: '30', + perPeriod: '100', + }; + const currentBlock = '30'; + const expectedOriginalLock = new BigNumber(3000); + const expectedFutureLock = new BigNumber(2800); - describe('toBN', () => { - it('returns default value for undefined', () => { - const value = toBN(undefined); - expect(value).toEqual(new BigNumber(constants.defaultValue)); - }); - }); - - describe('calculateFutureLock', () => { - it('can calculate future lock for one vesting schedule', () => { - const futureLock = calculateFutureLock(vestingSchedule, currentBlock); - - expect(futureLock).toEqual(new BigNumber(2800)); - }); - }); - - describe('calculateClaimableAmount', () => { - it('can calculate claimable amount', () => { - const claimableAmount = calculateClaimableAmount( - [vestingSchedule, vestingSchedule], - lockedTokens, + it('can calculate original- and future-lock for one vesting schedule', () => { + const [originalLock, futureLock] = calculateLock( + vestingSchedule, currentBlock ); - expect(claimableAmount).toEqual( - toBN(lockedTokens.amount).minus(toBN('2800').multipliedBy(2)) - ); + expect(originalLock).toEqual(expectedOriginalLock); + expect(futureLock).toEqual(expectedFutureLock); }); }); }); diff --git a/src/hooks/vesting/calculateClaimableAmount.tsx b/src/hooks/vesting/calculateClaimableAmount.tsx index 3a3fe821..859adbe7 100644 --- a/src/hooks/vesting/calculateClaimableAmount.tsx +++ b/src/hooks/vesting/calculateClaimableAmount.tsx @@ -1,10 +1,8 @@ import { ApiPromise } from '@polkadot/api'; -import { BalanceLock } from '@polkadot/types/interfaces'; import { Codec } from '@polkadot/types/types'; import BigNumber from 'bignumber.js'; import { find } from 'lodash'; -import constants from '../../constants'; -import { VestingSchedule } from './useGetVestingScheduleByAddress'; +import { VestingSchedule } from '../../generated/graphql'; export const balanceLockDataType = 'Vec'; export const tokensLockDataType = balanceLockDataType; @@ -33,9 +31,7 @@ export const getLockedBalanceByAddressAndLockId = async ( balanceLockDataType, await apiInstance.query.balances.locks(address) ), - (lockedAmount) => - // lockedAmount.id.eq(lockId) - false + (lockedAmount) => lockedAmount.id.eq(lockId) ); const tokenBalanceLocks = ( @@ -63,58 +59,70 @@ export const getLockedBalanceByAddressAndLockId = async ( }; /** - * This function casts a number in string representation - * to a BigNumber. If the input is undefined, it returns - * a default value. + * Calculates original and future lock for given VestingSchedule. + * https://gist.github.com/maht0rz/53466af0aefba004d5a4baad23f8ce26 + * + * returns [originalLock, futureLock] */ -export const toBN = (numberAsString: string | undefined) => { - // TODO: check if it is any good to use default values - // on undefined VestingSchedule properties! - if (!numberAsString) return new BigNumber(constants.defaultValue); - return new BigNumber(numberAsString); -}; +export const calculateLock = ( + vesting: VestingSchedule, + currentBlockNumber: string +): [BigNumber, BigNumber] => { + const startPeriod = new BigNumber(vesting.start); + const period = new BigNumber(vesting.period); -// https://gist.github.com/maht0rz/53466af0aefba004d5a4baad23f8ce26 -// TODO: check if calc makes sense for undefined VestingSchedule properties -export const calculateFutureLock = ( - vestingSchedule: VestingSchedule, - currentBlockNumber: BigNumber -) => { - const startPeriod = toBN(vestingSchedule.start); - const period = toBN(vestingSchedule.period); - const numberOfPeriods = currentBlockNumber + // if the vesting has not started, number of periods is 0 + let numberOfPeriods = new BigNumber(currentBlockNumber) .minus(startPeriod) .dividedBy(period); + numberOfPeriods = numberOfPeriods.isNegative() + ? new BigNumber('0') + : numberOfPeriods; - const perPeriod = toBN(vestingSchedule.perPeriod); + const perPeriod = new BigNumber(vesting.perPeriod); const vestedOverPeriods = numberOfPeriods.multipliedBy(perPeriod); - const periodCount = toBN(vestingSchedule.periodCount); + const periodCount = new BigNumber(vesting.periodCount); const originalLock = periodCount.multipliedBy(perPeriod); - const futureLock = originalLock.minus(vestedOverPeriods); - return futureLock; + const unlocked = vestedOverPeriods.gte(originalLock) + ? originalLock + : vestedOverPeriods; + const futureLock = originalLock.minus(unlocked); + + return [originalLock, futureLock]; }; -// get lockedVestingAmount from function getLockedBalanceByAddressAndLockId -export const calculateClaimableAmount = ( +/** + * Calculates originalLock and futureLock for every vesting schedule and + * sums it to total. + */ +export const calculateTotalLocks = ( vestingSchedules: VestingSchedule[], - lockedVestingAmount: BalanceLock | LockedTokens, - currentBlockNumber: BigNumber -): BigNumber => { - // calculate futureLock for every vesting schedule and sum to total - const totalFutureLocks = vestingSchedules.reduce(function ( - total, - vestingSchedule - ) { - const futureLock = calculateFutureLock(vestingSchedule, currentBlockNumber); - return total.plus(futureLock); - }, - new BigNumber(0)); - - // calculate claimable amount - const remainingVestingAmount = toBN(lockedVestingAmount?.amount?.toString()); - const claimableAmount = remainingVestingAmount.minus(totalFutureLocks); - - return claimableAmount; + currentBlockNumber: string +) => { + /** + * .reduce did not play well with an object that has multiple BigNumbers + * that's why the summation runs twice. + */ + const sumOriginalLock = vestingSchedules.reduce( + (accumulator, vestingSchedule) => { + const [originalLock] = calculateLock(vestingSchedule, currentBlockNumber); + return accumulator.plus(originalLock); + }, + new BigNumber(0) + ); + + const sumFutureLock = vestingSchedules.reduce( + (accumulator, vestingSchedule) => { + const [, futureLock] = calculateLock(vestingSchedule, currentBlockNumber); + return accumulator.plus(futureLock); + }, + new BigNumber(0) + ); + + return { + original: sumOriginalLock.toString(), + future: sumFutureLock.toString(), + }; }; diff --git a/src/hooks/vesting/graphql/Vesting.graphql b/src/hooks/vesting/graphql/Vesting.graphql new file mode 100644 index 00000000..1315c19e --- /dev/null +++ b/src/hooks/vesting/graphql/Vesting.graphql @@ -0,0 +1,29 @@ +# https://github.com/open-web3-stack/open-runtime-module-library/blob/master/vesting/src/lib.rs#L11 +type VestingSchedule { + # since this block + start: String! + # every `period` blocks + period: String! + # for number of periods + periodCount: String! + # claimable amount per period + perPeriod: String! +} + +extend type Query { + _vestingSchedule: VestingSchedule +} + +type Vesting { + claimableAmount: String! + originalLockBalance: String! + lockedVestingBalance: String! +} + +interface IVesting { + vesting: Vesting +} + +extend type Query implements IVesting { + vesting: Vesting +} diff --git a/src/hooks/vesting/graphql/VestingSchedule.graphql b/src/hooks/vesting/graphql/VestingSchedule.graphql deleted file mode 100644 index 7417a25f..00000000 --- a/src/hooks/vesting/graphql/VestingSchedule.graphql +++ /dev/null @@ -1,13 +0,0 @@ -# https://github.com/open-web3-stack/open-runtime-module-library/blob/master/vesting/src/lib.rs#L11 -type VestingSchedule { - # total locked amoount left to eventually be claimed - remainingVestingAmount: String, - # since this block - start: String, - # every `period` blocks - period: String, - # for number of periods - periodCount: String, - # claimable amount per period - perPeriod: String -} \ No newline at end of file diff --git a/src/hooks/vesting/useClaimVestedAmountMutation.tsx b/src/hooks/vesting/useClaimVestedAmountMutation.tsx index 41e37eac..e7950210 100644 --- a/src/hooks/vesting/useClaimVestedAmountMutation.tsx +++ b/src/hooks/vesting/useClaimVestedAmountMutation.tsx @@ -1,18 +1,15 @@ -import { useMutation } from '@apollo/client'; +import { MutationHookOptions, useMutation } from '@apollo/client'; import { loader } from 'graphql.macro'; export const CLAIM_VESTED_AMOUNT = loader('./graphql/ClaimVestedAmount.mutation.graphql'); export type ClaimVestedAmountMutationResponse = void; -export interface ClaimVestedAmountMutationVariables { - address?: string -} // no need to refetch queries, active account will refetch with every new block anyways -export const useClaimVestedAmountMutation = (variables?: ClaimVestedAmountMutationVariables) => useMutation( +export const useClaimVestedAmountMutation = (options?: MutationHookOptions) => useMutation( CLAIM_VESTED_AMOUNT, { - variables, notifyOnNetworkStatusChange: true, + ...options, } ) \ No newline at end of file diff --git a/src/hooks/vesting/useGetVestingByAddress.tsx b/src/hooks/vesting/useGetVestingByAddress.tsx new file mode 100644 index 00000000..894afa7e --- /dev/null +++ b/src/hooks/vesting/useGetVestingByAddress.tsx @@ -0,0 +1,92 @@ +import { useMemo } from 'react'; +import { usePolkadotJsContext } from '../polkadotJs/usePolkadotJs'; +import { Vec } from '@polkadot/types'; +import { VestingScheduleOf } from '@open-web3/orml-types/interfaces'; +import { ApiPromise } from '@polkadot/api'; +import { + calculateTotalLocks, + getLockedBalanceByAddressAndLockId, + vestingBalanceLockId, +} from './calculateClaimableAmount'; +import { readLastBlock } from '../lastBlock/readLastBlock'; +import { ApolloClient } from '@apollo/client'; +import BigNumber from 'bignumber.js'; +import { Query, Vesting, VestingSchedule } from '../../generated/graphql'; + +export const vestingScheduleDataType = 'Vec'; + +export const getVestingByAddressFactory = + (apiInstance?: ApiPromise) => + async ( + client: ApolloClient, + address?: string + ): Promise => { + if (!apiInstance || !address) return; + const currentBlockNumber = + readLastBlock(client)?.lastBlock?.relaychainBlockNumber; + if (!currentBlockNumber) + throw Error(`Can't calculate locks without current block number.`); + + // TODO: instead of multiple .createType calls, use the following + // https://github.com/AcalaNetwork/acala.js/blob/9634e2291f1723a84980b3087c55573763c8e82e/packages/sdk-core/src/functions/getSubscribeOrAtQuery.ts#L4 + const vestingSchedulesData = apiInstance.createType( + vestingScheduleDataType, + await apiInstance.query.vesting.vestingSchedules(address) + ) as Vec; + + const vestingSchedules = vestingSchedulesData.map((vestingSchedule) => { + return { + start: vestingSchedule?.start.toString(), + period: vestingSchedule?.period.toString(), + periodCount: vestingSchedule?.periodCount.toString(), + perPeriod: vestingSchedule?.perPeriod.toString(), + } as VestingSchedule; + }); + + const totalLocks = calculateTotalLocks( + vestingSchedules, + currentBlockNumber! + ); + + // 'ormlvest' is being fetched + const currentLockedVestingBalanceOrmlvest = ( + await getLockedBalanceByAddressAndLockId( + apiInstance, + address, + vestingBalanceLockId + ) + )?.amount?.toString(); + + if (!currentLockedVestingBalanceOrmlvest) + return { + claimableAmount: '0', + originalLockBalance: '0', + lockedVestingBalance: '0', + }; + + // TODO: add support for lockIds other than ormlvest + const currentLockedVestingOrmlvest = new BigNumber( + currentLockedVestingBalanceOrmlvest + ); + // claimable = currentRemainingVesting - all future locks + const claimableAmount = currentLockedVestingOrmlvest.minus( + new BigNumber(totalLocks.future) + ); + + return { + claimableAmount: claimableAmount.toString(), + originalLockBalance: totalLocks.original, // totalLocks.original == originalOrmlvestVesting + lockedVestingBalance: currentLockedVestingOrmlvest.toString(), + } as Vesting; + }; + +export const useGetVestingByAddress = () => { + const { apiInstance } = usePolkadotJsContext(); + + const getVestingByAddress = useMemo( + () => getVestingByAddressFactory(apiInstance), + [apiInstance] + ); + + return getVestingByAddress; +}; diff --git a/src/hooks/vesting/useVestingMutationResolvers.tsx b/src/hooks/vesting/useVestingMutationResolvers.tsx index 1775f18e..f9c02a89 100644 --- a/src/hooks/vesting/useVestingMutationResolvers.tsx +++ b/src/hooks/vesting/useVestingMutationResolvers.tsx @@ -2,7 +2,6 @@ import { useCallback } from 'react'; import { withErrorHandler } from '../apollo/withErrorHandler'; import { usePolkadotJsContext } from '../polkadotJs/usePolkadotJs'; import { web3FromAddress } from '@polkadot/extension-dapp'; -import { ClaimVestedAmountMutationVariables } from './useClaimVestedAmountMutation'; import { ExtrinsicStatus } from '@polkadot/types/interfaces/author'; import { DispatchError, EventRecord } from '@polkadot/types/interfaces/system'; import log from 'loglevel'; @@ -12,6 +11,7 @@ import { GET_ACTIVE_ACCOUNT, } from '../accounts/queries/useGetActiveAccountQuery'; import { ApiPromise } from '@polkadot/api'; +import { reject } from 'lodash'; /** * Run an async function and handle the thrown errors @@ -116,6 +116,38 @@ export const vestingClaimHandler = export const noAccountSelectedError = 'No Account selected'; export const polkadotJsNotReadyYetError = 'Polkadot.js is not ready yet'; +const claimVestingExtrinsic = (apiInstance: ApiPromise) => + apiInstance.tx.vesting.claim; + +// TODO: this should be generated with graphql +export interface ClaimVestedAmountMutationVariables { + address?: string +} + +const getAddress = ( + cache: ApolloCache, + args: ClaimVestedAmountMutationVariables +) => { + return args?.address + ? args.address + : cache.readQuery({ + query: GET_ACTIVE_ACCOUNT, + })?.activeAccount?.id; +}; + +export const estimateClaimVesting = async ( + cache: ApolloCache, + apiInstance: ApiPromise, + args: ClaimVestedAmountMutationVariables +) => { + const address = getAddress(cache, args); + + if (!address) + throw new Error(`Can't retrieve vesting address for estimation`); + + return claimVestingExtrinsic(apiInstance)().paymentInfo(address); +}; + export const useVestingMutationResolvers = () => { const { apiInstance, loading } = usePolkadotJsContext(); @@ -123,14 +155,10 @@ export const useVestingMutationResolvers = () => { useCallback( async ( _obj, - variables: ClaimVestedAmountMutationVariables, + args: ClaimVestedAmountMutationVariables, { cache }: { cache: ApolloCache } ) => { - const address = variables?.address - ? variables.address - : cache.readQuery({ - query: GET_ACTIVE_ACCOUNT, - })?.activeAccount?.id; + const address = getAddress(cache, args); // TODO: error handling? if (!address) throw new Error(noAccountSelectedError); @@ -139,29 +167,33 @@ export const useVestingMutationResolvers = () => { // // TODO: why does this not return a tx hash? // return await withGracefulErrors( - // async (resolve, reject) => { - // const { signer } = await web3FromAddress(address); - // await apiInstance.tx.vesting - // .claim() - // .signAndSend( - // address, - // { signer }, - // vestingClaimHandler(resolve, reject) - // ); - // }, - // [gracefulExtensionCancelationErrorHandler] + // async (resolve, reject) => { + // const { signer } = await web3FromAddress(address); + // await apiInstance.tx.vesting + // .claim() + // .signAndSend( + // address, + // { signer }, + // vestingClaimHandler(resolve, reject) + // ); + // }, + // [gracefulExtensionCancelationErrorHandler] // ); - return new Promise(async (resolve, reject) => { + await new Promise(async (resolve, reject) => { const { signer } = await web3FromAddress(address); - await apiInstance.tx.vesting - .claim() - .signAndSend( + try { + await claimVestingExtrinsic(apiInstance)().signAndSend( address, { signer }, vestingClaimHandler(resolve, reject) ); + } catch(e) { + reject(e) + } }); + + }, [loading, apiInstance] ), diff --git a/src/hooks/vesting/useVestingQueryResolvers.tsx b/src/hooks/vesting/useVestingQueryResolvers.tsx new file mode 100644 index 00000000..3e29fe52 --- /dev/null +++ b/src/hooks/vesting/useVestingQueryResolvers.tsx @@ -0,0 +1,24 @@ +import { ApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; +import { Account } from '../../generated/graphql'; +import { withErrorHandler } from '../apollo/withErrorHandler'; +import { useGetVestingByAddress } from './useGetVestingByAddress'; + +export const useVestingQueryResolvers = () => { + const getVestingByAddress = useGetVestingByAddress(); + const vesting = withErrorHandler( + useCallback( + async ( + account: Account, + _args: any, + { client }: { client: ApolloClient } + ) => await getVestingByAddress(client, account.id), + [getVestingByAddress] + ), + 'vesting' + ); + + return { + vesting, + }; +}; diff --git a/src/lang/en.json b/src/lang/en.json index fb74cba7..0dc93ab9 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -18,7 +18,7 @@ "defaultMessage": "Clear account" }, "Wallet.SelectAccount": { - "defaultMessage": "Select an account" + "defaultMessage": "Select account" }, "Wallet.InstallInstructions": { "defaultMessage": "To connect your account, please {link}." diff --git a/src/misc/colors.module.scss b/src/misc/colors.module.scss index 10860a96..5a466f39 100644 --- a/src/misc/colors.module.scss +++ b/src/misc/colors.module.scss @@ -19,7 +19,7 @@ $white: #ffffff; $gray1: #424250; $gray2: #211f24; $gray3: #26282f; -$gray4: #bdccd4; +$gray4: #a2b0bb; $gray5: #3b3a49; $gray6: #abb4c1; diff --git a/src/misc/defaults.scss b/src/misc/defaults.scss index 9301531c..7bf01c14 100644 --- a/src/misc/defaults.scss +++ b/src/misc/defaults.scss @@ -1,23 +1,32 @@ @import './colors.module.scss'; @import './misc.module.scss'; +@font-face { + font-family: 'Satoshi'; + src: url('./fonts/Satoshi-Variable.ttf') format('truetype-variations'); + font-weight: 300 900; +} + html, body { - font-family: 'Roboto', sans-serif; + font-family: 'Satoshi', sans-serif; /* Better Font Rendering */ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background: $d-gray3; - color: $white1; + background: radial-gradient( + 89.2% 89.2% at 50.07% 87.94%, + #008a69 0%, + #262f31 88.52% + ), + #2c3335; + + background-attachment: fixed; padding: 0; min-width: 360px; min-height: 400px; height: 100%; - - /* */ - background: linear-gradient(0deg, #2d2a32 0%, #34333e 100%); } div { diff --git a/src/misc/fonts/Satoshi-Variable.ttf b/src/misc/fonts/Satoshi-Variable.ttf new file mode 100644 index 00000000..976e85cb Binary files /dev/null and b/src/misc/fonts/Satoshi-Variable.ttf differ diff --git a/src/misc/icons/assets/AUSD.png b/src/misc/icons/assets/AUSD.png new file mode 100644 index 00000000..13af5392 Binary files /dev/null and b/src/misc/icons/assets/AUSD.png differ diff --git a/src/misc/misc.module.scss b/src/misc/misc.module.scss index eaa4ef52..341bc5e7 100644 --- a/src/misc/misc.module.scss +++ b/src/misc/misc.module.scss @@ -1,4 +1,4 @@ -$border-radius: 8px; +$border-radius: 16px; :export { border-radius: $border-radius; diff --git a/src/pages/PoolsPage/PoolsPage.scss b/src/pages/PoolsPage/PoolsPage.scss new file mode 100644 index 00000000..192a8cc1 --- /dev/null +++ b/src/pages/PoolsPage/PoolsPage.scss @@ -0,0 +1,15 @@ +@import '../../misc/misc.module.scss'; +@import '../../misc/colors.module.scss'; +@import '../TradePage/TradePage.scss'; + +.pools-page-wrapper { + @extend .trade-page-wrapper; +} + +.pools-page { + @extend .pools-page; +} + +.notifications-bar { + @extend .notifications-bar; +} diff --git a/src/pages/PoolsPage/PoolsPage.tsx b/src/pages/PoolsPage/PoolsPage.tsx new file mode 100644 index 00000000..2c16897b --- /dev/null +++ b/src/pages/PoolsPage/PoolsPage.tsx @@ -0,0 +1,350 @@ +import { NetworkStatus, useApolloClient } from '@apollo/client'; +import classNames from 'classnames'; +import { find, uniq, last } from 'lodash'; +import moment from 'moment'; +import { usePageVisibility } from 'react-page-visibility'; +import { + Dispatch, + SetStateAction, + useState, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; +import { Control, useForm, UseFormReturn } from 'react-hook-form'; +import { useSearchParams } from 'react-router-dom'; +import { AssetIds, Balance, Pool, TradeType } from '../../generated/graphql'; +import { readActiveAccount } from '../../hooks/accounts/lib/readActiveAccount'; +import { useGetActiveAccountQuery } from '../../hooks/accounts/queries/useGetActiveAccountQuery'; +import { useGetHistoricalBalancesQuery } from '../../hooks/balances/queries/useGetHistoricalBalancesQuery'; +import { useMath } from '../../hooks/math/useMath'; +import { useSubmitTradeMutation } from '../../hooks/pools/mutations/useSubmitTradeMutation'; +import { useGetPoolByAssetsQuery } from '../../hooks/pools/queries/useGetPoolByAssetsQuery'; +import { useAssetIdsWithUrl } from './hooks/useAssetIdsWithUrl'; +import { Line } from 'react-chartjs-2'; +import { fromPrecision12 } from '../../hooks/math/useFromPrecision'; +import { TradeChart as TradeChartComponent } from '../../components/Chart/TradeChart/TradeChart'; +import './PoolsPage.scss'; +import { + ChartGranularity, + ChartType, + PoolType, +} from '../../components/Chart/shared'; +import BigNumber from 'bignumber.js'; +import { useLoading } from '../../hooks/misc/useLoading'; +import { useGetPoolsQuery } from '../../hooks/pools/queries/useGetPoolsQuery'; + +import KSM from '../../misc/icons/assets/KSM.svg'; +import BSX from '../../misc/icons/assets/BSX.svg'; +import DAI from '../../misc/icons/assets/DAI.svg'; +import Unknown from '../../misc/icons/assets/Unknown.svg'; + +import { useGetActiveAccountTradeBalances } from './queries/useGetActiveAccountTradeBalances'; +// import { ConfirmationType, useWithConfirmation } from '../../hooks/actionLog/useWithConfirmation'; +import { horizontalBar } from '../../components/Chart/ChartHeader/ChartHeader'; +import { PoolsForm, PoolsFormFields, ProvisioningType } from '../../components/Pools/PoolsForm'; +import { idToAsset } from '../TradePage/TradePage'; +import { useRemoveLiquidityMutation } from '../../hooks/pools/mutations/useRemoveLiquidityMutation'; +import { useAddLiquidityMutation } from '../../hooks/pools/mutations/useAddLiquidityMutation'; +import Icon from '../../components/Icon/Icon'; + +export interface TradeAssetIds { + assetIn: string | null; + assetOut: string | null; +} + +export interface TradeChartProps { + pool?: Pool; + isPoolLoading?: boolean; + assetIds: TradeAssetIds; + spotPrice?: { + outIn?: string; + inOut?: string; + }; +} + +export const PoolsPage = () => { + // taking assetIn/assetOut from search params / query url + const [assetIds, setAssetIds] = useAssetIdsWithUrl(); + const { data: activeAccountData } = useGetActiveAccountQuery({ + fetchPolicy: 'cache-only', + }); + const { math } = useMath(); + // progress, not broadcast because we dont wait for broadcast to happen here + const [notification, setNotification] = useState< + 'standby' | 'pending' | 'success' | 'failed' + >('standby'); + + const depsLoading = useLoading(); + const { + data: poolData, + loading: poolLoading, + networkStatus: poolNetworkStatus, + } = useGetPoolByAssetsQuery( + { + assetInId: + (assetIds.assetIn! > assetIds.assetOut! + ? assetIds.assetIn + : assetIds.assetOut) || undefined, + assetOutId: + (assetIds.assetIn! > assetIds.assetOut! + ? assetIds.assetOut + : assetIds.assetIn) || undefined, + }, + depsLoading + ); + + const { + data: poolsData, + networkStatus: poolsNetworkStatus, + } = useGetPoolsQuery({ + skip: depsLoading, + }); + + const assets = useMemo(() => { + const assets = poolsData?.pools + ?.map((pool) => { + return [pool.assetInId, pool.assetOutId]; + }) + .reduce((assets, poolAssets) => { + return assets.concat(poolAssets); + }, []) + .map((id) => id); + + return uniq(assets).map((id) => ({ id })); + }, [poolsData]); + + const pool = useMemo(() => poolData?.pool, [poolData]); + + const isActiveAccountConnected = useMemo(() => { + return !!activeAccountData?.activeAccount; + }, [activeAccountData]); + + const clearNotificationIntervalRef = useRef(); + + // const { + // mutation: [ + // submitTrade, + // { loading: tradeLoading, error: tradeError }, + // ], + // confirmationScreen + // } = useWithConfirmation( + // useSubmitTradeMutation({ + // onCompleted: () => { + // setNotification('success'); + // clearNotificationIntervalRef.current = setTimeout(() => { + // setNotification('standby'); + // }, 4000); + // }, + // onError: () => { + // setNotification('failed'); + // clearNotificationIntervalRef.current = setTimeout(() => { + // setNotification('standby'); + // }, 4000); + // }, + // }), + // ConfirmationType.Trade + // ); + + const [ + removeLiquidity, + { loading: removeLiquidityLoading, error: removeLiquidityError }, + ] = useRemoveLiquidityMutation({ + onCompleted: () => { + setNotification('success'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + onError: () => { + setNotification('failed'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + }); + + const [ + addLiquidity, + { loading: addLiquidityLoading, error: addLiquidityLError }, + ] = useAddLiquidityMutation({ + onCompleted: () => { + setNotification('success'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + onError: () => { + setNotification('failed'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + }); + + console.log('removeLiquidityError', removeLiquidityError) + + useEffect(() => { + if (removeLiquidityLoading || addLiquidityLoading) setNotification('pending'); + }, [removeLiquidityLoading, addLiquidityLoading]); + + const handleSubmit = useCallback( + (variables: PoolsFormFields & { amountBMaxLimit?: string }) => { + clearNotificationIntervalRef.current && + clearTimeout(clearNotificationIntervalRef.current); + clearNotificationIntervalRef.current = null; + if (variables.provisioningType === ProvisioningType.Remove) { + console.log('removing liquidity', variables); + if (!variables.assetIn || !variables.assetOut || !variables.shareAssetAmount) return; + removeLiquidity({ + variables: { + assetA: variables.assetIn, + assetB: variables.assetOut, + amount: variables.shareAssetAmount + } + }); + } else { + console.log('adding liquidity', variables); + if (!variables.assetIn || !variables.assetOut || !variables.assetInAmount || !variables.amountBMaxLimit) return; + + addLiquidity({ + variables: { + assetA: variables.assetIn, + assetB: variables.assetOut, + amountA: variables.assetInAmount, + amountBMaxLimit: variables.amountBMaxLimit + } + }) + } + }, + [removeLiquidity] + ); + + const assetOutLiquidity = useMemo(() => { + const assetId = assetIds.assetOut || undefined; + return find(pool?.balances, { assetId })?.balance; + }, [pool, assetIds]); + + const assetInLiquidity = useMemo(() => { + const assetId = assetIds.assetIn || undefined; + return find(pool?.balances, { assetId })?.balance; + }, [pool, assetIds]); + + const spotPrice = useMemo(() => { + if (!assetOutLiquidity || !assetInLiquidity || !math) return; + let spotPrice = { + outIn: math.xyk.get_spot_price( + assetOutLiquidity, + assetInLiquidity, + '1000000000000' + ), + inOut: math.xyk.get_spot_price( + assetInLiquidity, + assetOutLiquidity, + '1000000000000' + ), + }; + + // spotPrice = { + // outIn: new BigNumber(spotPrice.outIn!).dividedBy(1000).toFixed(3), + // inOut: new BigNumber(spotPrice.inOut!).dividedBy(1000).toFixed(3) + // } + + console.log('limit spotPrice', spotPrice) + + return spotPrice; + }, [assetOutLiquidity, assetInLiquidity, math]); + + const { + data: activeAccountTradeBalancesData, + networkStatus: activeAccountTradeBalancesNetworkStatus, + } = useGetActiveAccountTradeBalances({ + variables: { + assetInId: + (assetIds.assetIn! > assetIds.assetOut! + ? assetIds.assetIn + : assetIds.assetOut) || undefined, + assetOutId: + (assetIds.assetIn! > assetIds.assetOut! + ? assetIds.assetOut + : assetIds.assetIn) || undefined, + shareTokenId: pool?.shareTokenId || undefined + }, + }); + + const tradeBalances = useMemo(() => { + const balances = activeAccountTradeBalancesData?.activeAccount?.balances; + + const outBalance = find(balances, { + assetId: assetIds.assetOut, + }) as Balance | undefined; + + const inBalance = find(balances, { + assetId: assetIds.assetIn, + }) as Balance | undefined; + + const shareBalance = find(balances, { + assetId: pool?.shareTokenId, + }) as Balance | undefined; + + console.log('share balance', balances, shareBalance); + + return { outBalance, inBalance, shareBalance }; + }, [activeAccountTradeBalancesData, assetIds, pool]); + + return ( +
+ {/* {confirmationScreen} */} +
+
Transaction {notification}
+
+ +
+
+
+ {/* */} + setAssetIds(assetIds)} + isActiveAccountConnected={isActiveAccountConnected} + pool={pool} + // first load and each time the asset ids (variables) change + isPoolLoading={ + poolNetworkStatus === NetworkStatus.loading || + poolNetworkStatus === NetworkStatus.setVariables || + depsLoading + } + assetInLiquidity={assetInLiquidity} + assetOutLiquidity={assetOutLiquidity} + spotPrice={spotPrice} + onSubmit={handleSubmit} + tradeLoading={removeLiquidityLoading || addLiquidityLoading} + assets={assets} + activeAccount={activeAccountData?.activeAccount} + activeAccountTradeBalances={tradeBalances} + activeAccountTradeBalancesLoading={ + activeAccountTradeBalancesNetworkStatus === NetworkStatus.loading || + activeAccountTradeBalancesNetworkStatus === + NetworkStatus.setVariables || + depsLoading + } + /> +
+
+ ); +}; diff --git a/src/pages/PoolsPage/graphql/GetActiveAccountTradeBalances.query.graphql b/src/pages/PoolsPage/graphql/GetActiveAccountTradeBalances.query.graphql new file mode 100644 index 00000000..667a372e --- /dev/null +++ b/src/pages/PoolsPage/graphql/GetActiveAccountTradeBalances.query.graphql @@ -0,0 +1,13 @@ +query GetActiveAccountTradeBalances($assetInId: String, $assetOutId: String, $shareTokenId: String) { + lastBlock @client { + parachainBlockNumber + relaychainBlockNumber + } + + activeAccount @client { + balances(assetIds: [$assetInId, $assetOutId, $shareTokenId]) { + assetId, + balance + } + } +} \ No newline at end of file diff --git a/src/pages/PoolsPage/hooks/useAssetIdsWithUrl.tsx b/src/pages/PoolsPage/hooks/useAssetIdsWithUrl.tsx new file mode 100644 index 00000000..372cab75 --- /dev/null +++ b/src/pages/PoolsPage/hooks/useAssetIdsWithUrl.tsx @@ -0,0 +1,31 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { useSearchParams, useNavigate, createSearchParams } from "react-router-dom"; +import { TradeAssetIds } from "../PoolsPage"; +import { useDebugBoxContext } from "./useDebugBox"; +import { idToAsset } from "../../TradePage/TradePage"; + +export const useAssetIdsWithUrl = (): [TradeAssetIds, Dispatch>] => { + const [searchParams] = useSearchParams(); + const assetOut = idToAsset(searchParams.get('assetOut')); + const assetIn = idToAsset(searchParams.get('assetIn')); + const [assetIds, setAssetIds] = useState({ + // with default values if the router params are empty + assetIn: assetIn?.id, + assetOut: assetOut?.id || '0' + }); + + const navigate = useNavigate(); + const { debugBoxEnabled } = useDebugBoxContext(); + + useEffect(() => { + assetIds.assetIn && assetIds.assetOut && navigate({ + search: `?${createSearchParams({ + assetIn: assetIds.assetIn, + assetOut: assetIds.assetOut, + ...(debugBoxEnabled ? { debug: 'true' } : null) + })}` + }); + }, [assetIds, searchParams, debugBoxEnabled]); + + return [assetIds, setAssetIds]; + } \ No newline at end of file diff --git a/src/pages/PoolsPage/hooks/useDebugBox.tsx b/src/pages/PoolsPage/hooks/useDebugBox.tsx new file mode 100644 index 00000000..6598198b --- /dev/null +++ b/src/pages/PoolsPage/hooks/useDebugBox.tsx @@ -0,0 +1,52 @@ +import constate from 'constate'; +import log from 'loglevel'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import ReactJson from 'react-json-view' +import classNames from 'classnames'; + +export const useDebugBox = () => { + const [searchParams] = useSearchParams(); + const debugBoxEnabled = !!searchParams.get('debug'); + const [debugData, setDebugData] = useState({}); + + const debugComponent = useCallback( + (component: string, data: any) => { + // setTimeout(() => {}) + setDebugData((debugData: any) => ({ + ...debugData, + [component]: data, + })); + }, + [setDebugData] + ); + + useEffect(() => { + if (debugBoxEnabled) log.setLevel('info'); + }, [debugBoxEnabled]); + + const [position, setPosition] = useState<'right' | 'left' | 'bottom'>('bottom'); + const [visible, setVisible] = useState(debugBoxEnabled); + + const debugBox = useMemo(() => { + if (!debugBoxEnabled) return <>; + return ( + //
{JSON.stringify(debugData, undefined, 2)}
+
+ + + + +
+ +
+
+ ); + }, [debugData, debugBoxEnabled, position, visible]); + + return { debugComponent, debugBox, debugBoxEnabled }; +}; + +export const [DebugBoxProvider, useDebugBoxContext] = constate(useDebugBox); diff --git a/src/pages/PoolsPage/queries/useGetActiveAccountTradeBalances.tsx b/src/pages/PoolsPage/queries/useGetActiveAccountTradeBalances.tsx new file mode 100644 index 00000000..48465baa --- /dev/null +++ b/src/pages/PoolsPage/queries/useGetActiveAccountTradeBalances.tsx @@ -0,0 +1,26 @@ +import { QueryHookOptions, useQuery } from '@apollo/client'; +import { loader } from 'graphql.macro'; +import { Balance } from '../../../generated/graphql'; +const GET_ACTIVE_ACCOUNT_TRADE_BALANCES = loader( + './../graphql/GetActiveAccountTradeBalances.query.graphql' +); + +export interface GetActiveAccountTradeBalancesQueryVariables { + assetInId?: string; + assetOutId?: string; + shareTokenId?: string; +} + +export interface GetActiveAccountTradeBalancesQueryResponse { + activeAccount?: { + balances: Balance[] + } +} + +export const useGetActiveAccountTradeBalances = ( + options: QueryHookOptions +) => + useQuery(GET_ACTIVE_ACCOUNT_TRADE_BALANCES, { + notifyOnNetworkStatusChange: true, + ...options + }); diff --git a/src/pages/TradePage/TradePage.scss b/src/pages/TradePage/TradePage.scss index 40a95fa8..4226ba4b 100644 --- a/src/pages/TradePage/TradePage.scss +++ b/src/pages/TradePage/TradePage.scss @@ -12,75 +12,171 @@ width: 100%; border-radius: $border-radius; - overflow: hidden; } .notifications-bar { display: flex; - justify-content: center; + flex-direction: row; + justify-content: space-between; + align-items: center; - position: absolute; - right: 0; - top: 0; - font-size: 14px; - font-weight: 600; - height: 50px; - padding: 0 16px; - width: 200px; + font-size: 16px; + line-height: 16px; + font-weight: 500; + // height: 64px; + padding: 24px 24px; + padding-left: 29px; + width: 330px; + z-index: 10; + color: $l-gray2; + // color: $white1; + + margin: 0 auto; background-color: $d-gray5; - border-radius: $border-radius; + border-radius: 7px; + // border-top-left-radius: 0px; + // border-bottom-left-radius: 0px;; + + transition: right 200ms ease, background-color, 200ms ease; + + top: 90px; + right: 20px; + position: fixed; + + .notification-cancel-wrapper { + width: fit-content; + + .notification-cancel-button { + border: none; + outline: none; + padding: 0; + background: none; + user-select: none; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + + svg { + // fill: $gray5; + path { + fill: $gray1; + } + + &:hover { + path { + fill: $gray4; + } + } + width: 10px; + height: 10px; + } + } + } - transition: top 200ms ease, background-color, 200ms ease; + opacity: 1; + visibility: visible; &.transaction-standby { top: 0; - background-color: $d-gray5; + background-color: transparent; + opacity: 0; + visibility: hidden; + transition: none; - .notification { - visibility: hidden; - } + // .notification { + // visibility: hidden; + // } + // .notification-cancel-wrapper { + // visibility: hidden; + // } } &.transaction-success { - top: -24px; - background-color: $green2; - color: $black; + // color: $green2; + &:before { + position: absolute; + content: ''; + left: 0px; + top: 0; + width: 7px; + height: 100%; + background: $green1; + border-top-left-radius: 7px; + border-bottom-left-radius: 7px; + opacity: 0.9; + } } &.transaction-failed { - top: -24px; - background-color: $red1; - color: $black; + // color: $red1; + + &:before { + position: absolute; + content: ''; + left: 0px; + top: 0; + width: 7px; + height: 100%; + background: $red1; + border-top-left-radius: 7px; + border-bottom-left-radius: 7px; + opacity: 0.9; + } } &.transaction-pending { - top: -24px; - background-color: $orange1; - color: $black; + display: flex; + // color: $orange1; - .notification { - display: flex; - line-height: 20px; - &:before { - content: ' '; - display: block; - width: 14px; - height: 14px; - margin: 4px 4px 0 0; - border-radius: 50%; - border: 2px solid $black; - border-color: $black transparent $black transparent; - animation: loader 1.2s linear infinite; - } - @keyframes loader { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } + &:before { + position: absolute; + content: ''; + left: 0px; + top: 0; + width: 7px; + height: 100%; + background: $orange1; + border-top-left-radius: 7px; + border-bottom-left-radius: 7px; + opacity: 0.9; + + background: linear-gradient( + 0deg, + #4fffb0 1.27%, + #b3ff8f 48.96%, + #ff984e 104.14% + ), + linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%), + linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%); } + + // .notification { + // display: flex; + // flex-direction: row; + // justify-content: center; + // align-items: center; + + // &:before { + // content: ' '; + // display: block; + // width: 14px; + // height: 14px; + // margin: 4px 4px 0 0; + // border-radius: 50%; + // border: 2px solid $black; + // border-color: $orange1 transparent $orange1 transparent; + // animation: loader 1.2s linear infinite; + // } + // @keyframes loader { + // 0% { + // transform: rotate(0deg); + // } + // 100% { + // transform: rotate(360deg); + // } + // } + // } } } diff --git a/src/pages/TradePage/TradePage.tsx b/src/pages/TradePage/TradePage.tsx index 93dae559..598ee7e5 100644 --- a/src/pages/TradePage/TradePage.tsx +++ b/src/pages/TradePage/TradePage.tsx @@ -1,7 +1,8 @@ import { NetworkStatus, useApolloClient } from '@apollo/client'; import classNames from 'classnames'; -import { find, uniq } from 'lodash'; +import { find, uniq, last } from 'lodash'; import moment from 'moment'; +import { usePageVisibility } from 'react-page-visibility'; import { Dispatch, SetStateAction, @@ -37,10 +38,13 @@ import { useGetPoolsQuery } from '../../hooks/pools/queries/useGetPoolsQuery'; import KSM from '../../misc/icons/assets/KSM.svg'; import BSX from '../../misc/icons/assets/BSX.svg'; -import DAI from '../../misc/icons/assets/DAI.svg'; +import AUSD from '../../misc/icons/assets/AUSD.png'; import Unknown from '../../misc/icons/assets/Unknown.svg'; import { useGetActiveAccountTradeBalances } from './queries/useGetActiveAccountTradeBalances'; +// import { ConfirmationType, useWithConfirmation } from '../../hooks/actionLog/useWithConfirmation'; +import { horizontalBar } from '../../components/Chart/ChartHeader/ChartHeader'; +import Icon from '../../components/Icon/Icon'; export interface TradeAssetIds { assetIn: string | null; @@ -66,27 +70,38 @@ export const idToAsset = (id: string | null) => { fullName: 'Basilisk', icon: BSX, }, - '1': { - id: '1', + '5': { + id: '5', symbol: 'KSM', fullName: 'Kusama', icon: KSM, }, - '2': { - id: '2', + '4': { + id: '4', symbol: 'aUSD', fullName: 'Acala USD', + icon: AUSD, + }, + '6': { + id: '6', + symbol: 'LP BSX/KSM', + fullName: 'BSX/KSM Share token', icon: Unknown, }, - '3': { - id: '3', - symbol: 'DAI', - fullName: 'DAI Stablecoin', - icon: DAI, + '7': { + id: '7', + symbol: 'LP BSX/aUSD', + fullName: 'BSX/aUSD Share token', + icon: Unknown, }, }; - return assetMetadata[id!] as any; + return assetMetadata[id!] as any || id && { + id, + symbol: horizontalBar, + fullName: `Unknown asset ${id}`, + icon: Unknown + }; }; export const TradeChart = ({ @@ -95,14 +110,18 @@ export const TradeChart = ({ spotPrice, isPoolLoading, }: TradeChartProps) => { + const isVisible = usePageVisibility(); + const [historicalBalancesRange, setHistoricalBalancesRange] = useState({ + from: moment().subtract(1, 'days').toISOString(), + to: moment().toISOString(), + }); const { math } = useMath(); const { data: historicalBalancesData, networkStatus: historicalBalancesNetworkStatus, } = useGetHistoricalBalancesQuery( { - from: useMemo(() => moment().subtract(1, 'days').toISOString(), []), - to: useMemo(() => moment().toISOString(), []), + ...historicalBalancesRange, quantity: 100, // defaulting to an empty string like this is bad, if we want to use skip we should type the variables differently poolId: pool?.id || '', @@ -121,6 +140,7 @@ export const TradeChart = ({ const [dataset, setDataset] = useState>(); const [datasetLoading, setDatasetLoading] = useState(true); + const [datasetRefreshing, setDatasetRefreshing] = useState(false); const assetOutLiquidity = useMemo(() => { const assetId = assetIds.assetOut || undefined; @@ -196,6 +216,7 @@ export const TradeChart = ({ }); setDataset(dataset); + setDatasetRefreshing(false); setDatasetLoading(false); }, [ historicalBalancesData?.historicalBalances, @@ -205,6 +226,43 @@ export const TradeChart = ({ assetIds, ]); + useEffect(() => { + const lastRecordOutdatedBy = 60000; + + if ( + !isVisible || + historicalBalancesLoading || + datasetRefreshing + ) + return; + + const refetchHistoricalBalancesData = () => { + if ( + isVisible && !historicalBalancesLoading && !datasetRefreshing && + (!dataset?.length || last(dataset).x <= new Date().getTime() - lastRecordOutdatedBy) + ) { + setDatasetRefreshing(true); + setHistoricalBalancesRange({ + from: moment().subtract(1, 'days').toISOString(), + to: moment().toISOString(), + }); + } + }; + + refetchHistoricalBalancesData(); + + const refetchData = setInterval(() => { + refetchHistoricalBalancesData(); + }, lastRecordOutdatedBy) + + return () => clearInterval(refetchData) + }, [ + dataset, + isVisible, + historicalBalancesLoading, + datasetRefreshing, + ]); + // useEffect(() => { // setDataset(dataset => { // if (!spotPrice || !dataset) return dataset; @@ -220,12 +278,16 @@ export const TradeChart = ({ // }) // }, [pool, spotPrice,]) - const _isPoolLoading = useMemo( - () => isPoolLoading || historicalBalancesLoading || datasetLoading, - [datasetLoading, isPoolLoading, historicalBalancesLoading] - ); + const _isPoolLoading = useMemo(() => { + if (!isPoolLoading || datasetRefreshing) return false; - console.log('graph loading status _isPoolLoading', _isPoolLoading); + return isPoolLoading || historicalBalancesLoading || datasetLoading; + }, [ + datasetRefreshing, + datasetLoading, + isPoolLoading, + historicalBalancesLoading, + ]); return ( { const clearNotificationIntervalRef = useRef(); - const [ - submitTrade, - { loading: tradeLoading, error: tradeError }, - ] = useSubmitTradeMutation({ - onCompleted: () => { - setNotification('success'); - clearNotificationIntervalRef.current = setTimeout(() => { - setNotification('standby'); - }, 4000); - }, - onError: () => { - setNotification('failed'); - clearNotificationIntervalRef.current = setTimeout(() => { - setNotification('standby'); - }, 4000); - }, - }); + const [submitTrade, { loading: tradeLoading, error: tradeError }] = + useSubmitTradeMutation({ + onCompleted: () => { + setNotification('success'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + onError: () => { + setNotification('failed'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + }); useEffect(() => { if (tradeLoading) setNotification('pending'); @@ -393,11 +453,20 @@ export const TradePage = () => { return (
+ {/* {confirmationScreen} */}
-
transaction {notification}
+
Transaction {notification}
+
+ +
- { poolNetworkStatus === NetworkStatus.setVariables || depsLoading } - /> + /> */} setAssetIds(assetIds)} diff --git a/src/pages/TradePage/hooks/useAssetIdsWithUrl.tsx b/src/pages/TradePage/hooks/useAssetIdsWithUrl.tsx index 3b322780..0a09299b 100644 --- a/src/pages/TradePage/hooks/useAssetIdsWithUrl.tsx +++ b/src/pages/TradePage/hooks/useAssetIdsWithUrl.tsx @@ -16,7 +16,6 @@ export const useAssetIdsWithUrl = (): [TradeAssetIds, Dispatch { assetIds.assetIn && assetIds.assetOut && navigate({ search: `?${createSearchParams({ diff --git a/src/pages/WalletPage.tsx b/src/pages/WalletPage.tsx deleted file mode 100644 index 32c9a9a5..00000000 --- a/src/pages/WalletPage.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useMemo } from 'react'; -import { Account as AccountModel } from '../generated/graphql'; -import { useSetActiveAccountMutation } from '../hooks/accounts/mutations/useSetActiveAccountMutation'; -import { useGetAccountsQuery } from '../hooks/accounts/queries/useGetAccountsQuery'; -import { usePersistActiveAccount } from '../hooks/accounts/lib/usePersistActiveAccount'; - -export const Account = ({ account }: { account?: AccountModel }) => { - // TODO: you can get the loading state of the mutation here as well - // but it probably needs to be turned into a contextual mutation - // in order to share the loading state accross multiple mutation hook calls - const [setActiveAccount] = useSetActiveAccountMutation(); - - const { persistedActiveAccount } = usePersistActiveAccount(); - - return ( -
-

- {account?.name} - {account?.id === persistedActiveAccount?.id ? ' [active]' : <>} -

-

- Address: - {account?.id} -

-
- Balances: - {account?.balances.map((balance, i) => ( -

- {balance.assetId}: {balance.balance} -

- ))} -
- -
- ); -}; - -export const WalletPage = () => { - const { data: accountsData, loading: accountsLoading } = - useGetAccountsQuery(false); - - const loading = useMemo(() => { - return accountsLoading; - }, [accountsLoading]); - - return ( -
-

Accounts

- - {loading ? ( - [WalletPage] Loading accounts... - ) : ( - [WalletPage] Everything is up to date - )} - -
-
- - { -
- {accountsData?.accounts?.map((account, i) => ( - - ))} -
- } -
- ); -}; diff --git a/src/pages/WalletPage/WalletPage.scss b/src/pages/WalletPage/WalletPage.scss new file mode 100644 index 00000000..eafd2dbd --- /dev/null +++ b/src/pages/WalletPage/WalletPage.scss @@ -0,0 +1,22 @@ +@import '../../misc/colors.module.scss'; +@import '../../misc/misc.module.scss'; +@import '../TradePage/TradePage.scss'; + +.wallet-page { + min-width: 800px; + max-width: 1200px; + padding: 32px; + color: white; + margin: 0px auto; + color: white; + + .modal-button-container { + width: 100%; + display: flex; + justify-content: center; + } + + .notifications-bar { + @extend .notifications-bar; + } +} diff --git a/src/pages/WalletPage/WalletPage.tsx b/src/pages/WalletPage/WalletPage.tsx new file mode 100644 index 00000000..55ccf681 --- /dev/null +++ b/src/pages/WalletPage/WalletPage.tsx @@ -0,0 +1,190 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + Account, + Account as AccountModel, + Balance, + Maybe, + Vesting, + VestingSchedule, +} from '../../generated/graphql'; +import { useSetActiveAccountMutation } from '../../hooks/accounts/mutations/useSetActiveAccountMutation'; +import { useGetAccountsQuery } from '../../hooks/accounts/queries/useGetAccountsQuery'; +import { usePersistActiveAccount } from '../../hooks/accounts/lib/usePersistActiveAccount'; +import { + useGetActiveAccountQuery, + useGetActiveAccountQueryContext, +} from '../../hooks/accounts/queries/useGetActiveAccountQuery'; +import { NetworkStatus } from '@apollo/client'; +import { useLoading } from '../../hooks/misc/useLoading'; +import { + useGetExtensionQuery, + useGetExtensionQueryContext, +} from '../../hooks/extension/queries/useGetExtensionQuery'; +import { useModalPortalElement } from '../../components/Wallet/AccountSelector/hooks/useModalPortalElement'; +import { useAccountSelectorModal } from '../../containers/Wallet/hooks/useAccountSelectorModal'; +import { FormattedBalance } from '../../components/Balance/FormattedBalance/FormattedBalance'; +import BigNumber from 'bignumber.js'; +import { fromPrecision12 } from '../../hooks/math/useFromPrecision'; +import { useClaimVestedAmountMutation } from '../../hooks/vesting/useClaimVestedAmountMutation'; +import { BalanceList } from './containers/WalletPage/BalanceList/BalanceList'; +import { VestingClaim } from './containers/WalletPage/VestingClaim/VestingClaim'; +import { ActiveAccount } from './containers/WalletPage/ActiveAccount/ActiveAccount'; +import { useTransferFormModalPortal } from './containers/WalletPage/TransferForm/hooks/useTransferFormModalPortal'; +import { useSetConfigMutation } from '../../hooks/config/useSetConfigMutation'; +import { useGetConfigQuery } from '../../hooks/config/useGetConfigQuery'; +import './WalletPage.scss'; +import Icon from '../../components/Icon/Icon'; + +export type Notification = 'standby' | 'pending' | 'success' | 'failed'; + +export const WalletPage = () => { + const [notification, setNotification] = useState< + 'standby' | 'pending' | 'success' | 'failed' + >('standby'); + + const { data: extensionData, loading: extensionLoading } = + useGetExtensionQueryContext(); + + const depsLoading = useLoading(); + const { data: activeAccountData, networkStatus: activeAccountNetworkStatus } = + useGetActiveAccountQueryContext(); + + const activeAccount = useMemo( + () => activeAccountData?.activeAccount, + [activeAccountData] + ); + const activeAccountLoading = useMemo( + () => depsLoading || activeAccountNetworkStatus === NetworkStatus.loading, + [depsLoading, activeAccountNetworkStatus] + ); + + const { data: configData, networkStatus: configNetworkStatus } = + useGetConfigQuery({ + skip: activeAccountLoading, + }); + + const configLoading = useMemo(() => { + return depsLoading || configNetworkStatus == NetworkStatus.loading; + }, [configNetworkStatus, depsLoading]); + + // couldnt really quickly figure out how to use just activeAccount + extension loading states + // so depsLoading is reused here as well + const loading = useMemo( + () => + activeAccountLoading || extensionLoading || depsLoading || configLoading, + [activeAccountLoading, extensionLoading, depsLoading, configLoading] + ); + + const modalContainerRef = useRef(null); + const { modalPortal, openModal } = useAccountSelectorModal({ + modalContainerRef, + }); + + const assets = useMemo(() => { + return activeAccount?.balances.map((balance) => ({ id: balance.assetId })); + }, [activeAccount]); + + const { + modalPortal: transferFormModalPortal, + openModal: openTransferFormModalPortal, + } = useTransferFormModalPortal(modalContainerRef, setNotification, assets); + + const handleOpenTransformForm = useCallback( + (assetId: string, balance: string) => { + console.log('handleOpenTransformForm with asset id', assetId); + console.log('handleOpenTransformForm with balance', balance); + openTransferFormModalPortal({ assetId, balance }); + }, + [openTransferFormModalPortal] + ); + + const [setConfigMutation, { loading: setConfigLoading }] = + useSetConfigMutation(); + const clearNotificationIntervalRef = useRef(); + + useEffect(() => { + if (setConfigLoading) setNotification('pending'); + }, [setConfigLoading]); + + const onSetAsFeePaymentAsset = useCallback( + (feePaymentAsset: string) => { + clearNotificationIntervalRef.current && + clearTimeout(clearNotificationIntervalRef.current); + clearNotificationIntervalRef.current = null; + + console.log('setting fee payment asset', feePaymentAsset); + setConfigMutation({ + onCompleted: () => { + setNotification('success'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + onError: () => { + setNotification('failed'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + variables: { + config: { + feePaymentAsset, + }, + }, + }); + }, + [setConfigMutation] + ); + + return ( +
+
+ {modalPortal} + {transferFormModalPortal} +
+
Transaction {notification}
+
+ +
+
+
+ {loading ? ( +
+
+
Wallet loading...
+
+
+ ) : ( +
+ {activeAccount ? ( + <> + + + ) : ( +
+
openModal()}> +
+ Click here to connect an account +
+
+
+ )} +
+ )} +
+
+ ); +}; diff --git a/src/pages/WalletPage/containers/WalletPage/ActiveAccount/ActiveAccount.scss b/src/pages/WalletPage/containers/WalletPage/ActiveAccount/ActiveAccount.scss new file mode 100644 index 00000000..b5287a4b --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/ActiveAccount/ActiveAccount.scss @@ -0,0 +1,109 @@ +@import '../../../../../misc/colors.module.scss'; +@import '../../../../../misc/misc.module.scss'; + +.active-account { + min-width: 800px; + max-width: 1200px; + border-radius: $border-radius; + background: linear-gradient(180deg, #1c2527 0%, #14161a 80.73%, #121316 100%); + padding: 32px 0px 0px 0px; + color: white; + margin: 0px 0px 50px 0px; + display: flex; + flex-direction: column; + position: relative; + + &__title { + width: fit-content; + color: $l-gray3; + font-size: 22px; + font-weight: 500; + margin: 0px 32px 24px 32px; + background: linear-gradient( + 90deg, + #4fffb0 1.27%, + #b3ff8f 48.96%, + #ff984e 104.14% + ), + linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%), + linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + &-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 20px 32px; + font-size: 18px; + font-weight: 500; + &:nth-of-type(1) { + border-top: 1px solid rgb(41, 41, 45); + } + + &:nth-child(odd) { + background: rgba(255, 255, 255, 0.06); + } + &:last-child { + border-radius: 0px 0px $border-radius $border-radius; + } + + .item { + width: 50%; + display: flex; + flex-direction: row; + justify-content: left; + align-items: center; + gap: 10px; + + &:first-child { + width: 20%; + } + &:last-child { + width: 30%; + justify-content: right; + } + } + } + + &-button { + height: 40px; + user-select: none; + border-radius: 9999px; + background-color: rgba(76, 243, 168, 0.12); + color: #4fffb0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border: none; + cursor: pointer; + + &:hover { + background: rgba(76, 243, 168, 0.3); + } + + &__label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 8px 16px; + font-size: 16px; + line-height: 16px; + font-weight: 600; + } + } + + &-actions { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + } +} diff --git a/src/pages/WalletPage/containers/WalletPage/ActiveAccount/ActiveAccount.tsx b/src/pages/WalletPage/containers/WalletPage/ActiveAccount/ActiveAccount.tsx new file mode 100644 index 00000000..daafdabd --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/ActiveAccount/ActiveAccount.tsx @@ -0,0 +1,146 @@ +import Identicon from '@polkadot/react-identicon'; +import { encodeAddress, decodeAddress } from '@polkadot/util-crypto'; +import { useCallback } from 'react'; +import { + genesisHashToChain, + sourceToHuman, +} from '../../../../../components/Wallet/AccountSelector/AccountItem/AccountItem'; +import { Account, Maybe } from '../../../../../generated/graphql'; +import { useSetActiveAccountMutation } from '../../../../../hooks/accounts/mutations/useSetActiveAccountMutation'; +import { Notification } from '../../../WalletPage'; +import { BalanceList } from '../BalanceList/BalanceList'; +import { VestingClaim } from '../VestingClaim/VestingClaim'; +import './ActiveAccount.scss'; + +export const ActiveAccount = ({ + account, + loading, + onOpenAccountSelector, + onOpenTransferForm, + onSetAsFeePaymentAsset, + feePaymentAssetId, + setNotification, +}: { + account?: Maybe; + loading: boolean; + feePaymentAssetId?: Maybe; + onOpenAccountSelector: () => void; + onOpenTransferForm: (assetId: string, balance: string) => void; + onSetAsFeePaymentAsset: (assetId: string) => void; + setNotification: (notification: Notification) => void; +}) => { + const [setActiveAccount] = useSetActiveAccountMutation(); + + const handleClearAccount = useCallback(() => { + setActiveAccount({ variables: { id: undefined } }); + }, [setActiveAccount]); + + return ( + <> + {loading ? ( +
Loading...
+ ) : account ? ( + <> +
+

Active account

+
+
Name
+
Address
+
+
+
+
+
+
+ {account.name} +
+
+ {sourceToHuman(account.source)} +
+
+
+
+
+
+ +
+
Basilisk
+
+ {account.id} +
+
+
+ {genesisHashToChain(account.genesisHash).network !== + 'basilisk' ? ( +
+ +
+
+ {genesisHashToChain(account.genesisHash).displayName} +
+
+ {encodeAddress( + decodeAddress(account.id), + genesisHashToChain(account.genesisHash)?.prefix + )} +
+
+
+ ) : ( + <> + )} +
+
+
+
onOpenAccountSelector()} + > +
+ Change account +
+
+
handleClearAccount()} + > +
+ Clear account +
+
+
+
+
+ + {account?.vesting && ( + + )} + + + + ) : ( +
Please connect a wallet first
+ )} + + ); +}; diff --git a/src/pages/WalletPage/containers/WalletPage/BalanceList/BalanceList.scss b/src/pages/WalletPage/containers/WalletPage/BalanceList/BalanceList.scss new file mode 100644 index 00000000..49e66776 --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/BalanceList/BalanceList.scss @@ -0,0 +1,41 @@ +@import '../../../../../misc/colors.module.scss'; +@import '../../../../../misc/misc.module.scss'; +@import '../ActiveAccount/ActiveAccount.scss'; + +.balance-list { + @extend .active-account; + + &__title { + @extend .active-account__title; + } + + &-wrapper { + @extend .active-account-wrapper; + + .item { + width: 30%; + display: flex; + flex-direction: row; + justify-content: left; + align-items: center; + gap: 10px; + + &:last-child { + width: 40%; + justify-content: right; + } + } + } + + &-button { + @extend .active-account-button; + &__label { + @extend .active-account-button__label; + } + } + + &-actions { + @extend .active-account-actions; + justify-content: right; + } +} diff --git a/src/pages/WalletPage/containers/WalletPage/BalanceList/BalanceList.tsx b/src/pages/WalletPage/containers/WalletPage/BalanceList/BalanceList.tsx new file mode 100644 index 00000000..7eae4713 --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/BalanceList/BalanceList.tsx @@ -0,0 +1,68 @@ +import { Balance, Maybe } from '../../../../../generated/graphql'; +import { FormattedBalance } from '../../../../../components/Balance/FormattedBalance/FormattedBalance'; +import { idToAsset } from '../../../../TradePage/TradePage'; +import { horizontalBar } from '../../../../../components/Chart/ChartHeader/ChartHeader'; +import './BalanceList.scss'; + +export const availableFeePaymentAssetIds = ['0', '4', '5']; + +export const BalanceList = ({ + balances, + onOpenTransferForm, + onSetAsFeePaymentAsset, + feePaymentAssetId, +}: { + balances?: Array; + feePaymentAssetId?: Maybe; + onOpenTransferForm: (assetId: string, balance: string) => void; + onSetAsFeePaymentAsset: (assetId: string) => void; +}) => { + return ( +
+

Balance

+
+
Asset Name
+
Balance
+
+
+ {/* TODO: ordere by assetId? */} + {balances?.map((balance) => ( +
+
+ {idToAsset(balance.assetId || null)?.fullName || + `Unknown asset (ID: ${balance.assetId})`} + + {feePaymentAssetId === balance.assetId ? ' - fee asset' : ''} +
+ +
+ {/* TODO: how to deal with unknown assets? (not knowing the metadata e.g. symbol/fullname) */} + +
+
+ {availableFeePaymentAssetIds.includes(balance.assetId) ? ( + + ) : ( + <> + )} + +
+
+ ))} +
+ ); +}; diff --git a/src/pages/WalletPage/containers/WalletPage/TransferForm/TransferForm.scss b/src/pages/WalletPage/containers/WalletPage/TransferForm/TransferForm.scss new file mode 100644 index 00000000..e6ac873d --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/TransferForm/TransferForm.scss @@ -0,0 +1,151 @@ +@import '../../../../../misc/colors.module.scss'; +@import '../../../../../misc/misc.module.scss'; + +.transfer-form { + position: fixed; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + + background: rgba(50, 50, 50, 0.5); + color: white; + + z-index: 3; + + .validation { + opacity: 0; + line-height: 16px; + padding: 0 16px; + height: 100%; + max-height: 0px; + overflow: hidden; + margin-bottom: 14px; + margin-top: 4px; + + transition: max-height 0.3s ease, opacity 0.3s ease, padding 0.3s ease; + border-radius: 8px; + + &.visible { + max-height: 80px; + padding: 16px; + opacity: 1; + } + + &.error { + background: rgba(255, 104, 104, 0.3); + } + + &.warning { + color: $orange1; + } + } + + .transfer-form-container { + width: 460px; + &.modal-component-wrapper .modal-component-content { + padding: 0; + } + } + + &__content-wrapper { + min-height: fit-content; + max-height: 85vh; + padding: 16px; + + border-radius: $border-radius; + background: linear-gradient( + 180deg, + #1c2527 0%, + #14161a 80.73%, + #121316 100% + ); + position: relative; + } + + .transfer-form-heading { + display: flex; + justify-content: space-between; + width: 100%; + padding: 8px 16px 0px 16px; + } + + .transfer-form-title { + width: fit-content; + padding-top: 4px; + color: $l-gray3; + font-size: 22px; + font-weight: 500; + background: linear-gradient( + 90deg, + #4fffb0 1.27%, + #b3ff8f 48.96%, + #ff984e 104.14% + ), + linear-gradient(90deg, #4fffb0 1.27%, #a2ff76 53.24%, #ff984e 104.14%), + linear-gradient(90deg, #ffce4f 1.27%, #4fffb0 104.14%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; + } + + &__transfer-form-parent { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + } + + &__transfer-form-fee { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 10px; + color: #bdccd4; + font-size: 15px; + font-weight: 400; + } + + &__transfer-form-address-input { + border-radius: $border-radius; + height: 52px; + flex-grow: 1; + font-weight: 600; + padding: 16px 12px; + background-color: rgba(218, 255, 238, 0.06); + box-shadow: 0 0 0 1px rgb(255 255 238 / 30%); + } + + &__submit-button { + user-select: none; + border-radius: 9999px; + width: 100%; + height: 50px; + background-color: #4fffb0; + color: #26282f; + + &:hover { + background-color: #41db96; + } + } + + &__submit-button:disabled { + background-color: rgba(255, 255, 255, 0.2); + color: #a2b0b8; + } + + &__transfer-form-asset-input-container { + display: flex; + flex-direction: column; + background: rgba(162, 176, 187, 0.1); + color: $green1; + padding: 12px; + border-radius: 10px; + gap: 6px; + } +} diff --git a/src/pages/WalletPage/containers/WalletPage/TransferForm/TransferForm.tsx b/src/pages/WalletPage/containers/WalletPage/TransferForm/TransferForm.tsx new file mode 100644 index 00000000..50441a5a --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/TransferForm/TransferForm.tsx @@ -0,0 +1,252 @@ +import { useApolloClient } from '@apollo/client'; +import { watch } from 'fs'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { AssetBalanceInput } from '../../../../../components/Balance/AssetBalanceInput/AssetBalanceInput'; +import { FormattedBalance } from '../../../../../components/Balance/FormattedBalance/FormattedBalance'; +import Icon from '../../../../../components/Icon/Icon'; +import { useMultiFeePaymentConversionContext } from '../../../../../containers/MultiProvider'; +import { Asset } from '../../../../../generated/graphql'; +import { estimateBalanceTransfer } from '../../../../../hooks/balances/resolvers/mutation/balanceTransfer'; +import { useTransferBalanceMutation } from '../../../../../hooks/balances/resolvers/useTransferMutation'; +import { usePolkadotJsContext } from '../../../../../hooks/polkadotJs/usePolkadotJs'; +import { Notification } from '../../../WalletPage'; +import './TransferForm.scss'; +import { checkAddress } from '@polkadot/util-crypto'; +import constants from '../../../../../constants'; +import BigNumber from 'bignumber.js'; +import { toPrecision12 } from '../../../../../hooks/math/useToPrecision'; +import { fromPrecision12 } from '../../../../../hooks/math/useFromPrecision'; +import { encodeAddress, decodeAddress } from '@polkadot/util-crypto'; + + +export const TransferForm = ({ + closeModal, + assetId = '0', + balance = '0', + setNotification, + assets, +}: { + closeModal: () => void; + assetId?: string; + balance?: string; + setNotification: (notification: Notification) => void; + assets?: Asset[]; +}) => { + const modalContainerRef = useRef(null); + const form = useForm<{ + to?: string, + amount?: string, + asset?: string, + submit: any + }>({ + // mode: 'all', + defaultValues: { + asset: assetId, + to: undefined, + amount: undefined, + submit: undefined, + }, + }); + + const { + register, + watch, + getValues, + setValue, + trigger, + control, + formState, + } = form; + + const { isValid, isDirty, errors } = formState; + + const [transferBalance] = useTransferBalanceMutation(); + + const clearNotificationIntervalRef = useRef(); + const handleSubmit = useCallback( + (data: any) => { + // this is not ideal, but we want to show the pending status + // which is hidden behind the modal currently + closeModal(); + setNotification('pending'); + transferBalance({ + variables: { + currencyId: data.asset, + amount: data.amount, + to: data.to, + }, + onCompleted: () => { + setNotification('success'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + onError: () => { + setNotification('failed'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + }); + }, + [closeModal, setNotification, transferBalance] + ); + + useEffect(() => { + form.trigger('submit'); + }, [...form.watch(['amount', 'to', 'asset'])]); + + const [txFee, setTxFee] = useState(); + const { apiInstance, loading: apiInstanceLoading } = usePolkadotJsContext(); + const client = useApolloClient(); + const { convertToFeePaymentAsset, feePaymentAsset } = + useMultiFeePaymentConversionContext(); + + useEffect(() => { + if (!apiInstance || apiInstanceLoading) return; + (async () => { + console.log('reestimating', { + from: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + to: + form.getValues('to') || + '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + currencyId: form.getValues('asset') || '0', + amount: form.getValues('amount') || '0', + }); + const estimate = await estimateBalanceTransfer( + client.cache, + apiInstance, + { + from: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + to: + form.getValues('to') || + '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + currencyId: form.getValues('asset') || '0', + amount: form.getValues('amount') || '0', + } + ); + setTxFee(estimate.partialFee.toString()); + })(); + }, [ + apiInstance, + apiInstanceLoading, + client, + ...form.watch(['amount', 'asset', 'to']), + ]); + + const [displayError, setDisplayError] = useState(); + const isError = useMemo(() => !!errors?.submit?.type, [errors?.submit]); + const formError = useMemo(() => { + console.log('form.formState.errors?.submit?.type', form.formState.errors?.submit?.type) + switch (form.formState.errors?.submit?.type) { + case 'notEnoughBalance': + return 'Insufficient balance' + case 'address': + return 'Incorrect address' + case 'amount': + return 'Amount must be more than zero' + } + return; + }, [form.formState.errors.submit]); + + useEffect(() => { + if (formError) { + const timeoutId = setTimeout(() => setDisplayError(formError), 50); + return () => timeoutId && clearTimeout(timeoutId); + } + const timeoutId = setTimeout(() => setDisplayError(formError), 300); + return () => timeoutId && clearTimeout(timeoutId); + }, [formError]); + + return ( + <> +
+
+
+
+
Transfer
+
closeModal()}> + +
+
+ + +
+
+
+ + {/* TODO: validate address */} + +
+ +
+ + +
+ {/* Form state: {form.formState.isDirty ? 'dirty': 'clean'}, {form.formState.isValid ? 'valid' : 'invalid'} */} +
+ Tx fee:{' '} + {txFee && feePaymentAsset ? ( + + ) : ( + <>- + )} +
+
+
+ {displayError} +
+
+ { + const recipientAddress = form.getValues('to'); + + try { + decodeAddress(recipientAddress); + return true; + } catch (e) { + return false; + } + }, + amount: () => form.getValues('amount') !== undefined, + notEnoughBalance: () => { + const amount = form.getValues('amount'); + + return new BigNumber(fromPrecision12(balance) || 0).gte( + fromPrecision12(amount || '0')! + ); + }, + }, + })} + /> +
+
+
+
+
+ + ); +}; diff --git a/src/pages/WalletPage/containers/WalletPage/TransferForm/hooks/useTransferFormModalPortal.tsx b/src/pages/WalletPage/containers/WalletPage/TransferForm/hooks/useTransferFormModalPortal.tsx new file mode 100644 index 00000000..13b3df35 --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/TransferForm/hooks/useTransferFormModalPortal.tsx @@ -0,0 +1,21 @@ +import { useCallback, useRef } from "react" +import { ModalPortalElementFactory, useModalPortal } from "../../../../../../components/Balance/AssetBalanceInput/hooks/useModalPortal" +import { Asset } from "../../../../../../generated/graphql" +import { Notification } from "../../../../WalletPage" +import { TransferForm } from "../TransferForm" + +export const useModalPortalElement = (setNotification: (notification: Notification) => void, assets?: Asset[]) => { + return useCallback>(({ isModalOpen, closeModal, state }) => { + return isModalOpen + ? + : <> + }, [assets, setNotification]) +} + +export const useTransferFormModalPortal = (container: any, setNotification: (notification: Notification) => void, assets?: Asset[]) => { + + return useModalPortal( + useModalPortalElement(setNotification, assets), + container + ) +} \ No newline at end of file diff --git a/src/pages/WalletPage/containers/WalletPage/VestingClaim/VestingClaim.scss b/src/pages/WalletPage/containers/WalletPage/VestingClaim/VestingClaim.scss new file mode 100644 index 00000000..d4c19eaf --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/VestingClaim/VestingClaim.scss @@ -0,0 +1,36 @@ +@import '../../../../../misc/colors.module.scss'; +@import '../../../../../misc/misc.module.scss'; +@import '../ActiveAccount/ActiveAccount.scss'; + +.vesting-claim { + @extend .active-account; + + &__title { + @extend .active-account__title; + } + + &-wrapper { + @extend .active-account-wrapper; + + .item { + width: 22.5%; + &:last-child { + width: 10%; + justify-content: right; + } + } + } + + &-button { + @extend .active-account-button; + &__label { + @extend .active-account-button__label; + } + } + + &__fee { + width: 100px; + display: flex; + justify-content: left; + } +} diff --git a/src/pages/WalletPage/containers/WalletPage/VestingClaim/VestingClaim.tsx b/src/pages/WalletPage/containers/WalletPage/VestingClaim/VestingClaim.tsx new file mode 100644 index 00000000..98d70bb6 --- /dev/null +++ b/src/pages/WalletPage/containers/WalletPage/VestingClaim/VestingClaim.tsx @@ -0,0 +1,127 @@ +import { useApolloClient } from '@apollo/client'; +import BigNumber from 'bignumber.js'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FormattedBalance } from '../../../../../components/Balance/FormattedBalance/FormattedBalance'; +import { useMultiFeePaymentConversionContext } from '../../../../../containers/MultiProvider'; +import { Maybe, Vesting } from '../../../../../generated/graphql'; +import { fromPrecision12 } from '../../../../../hooks/math/useFromPrecision'; +import { usePolkadotJsContext } from '../../../../../hooks/polkadotJs/usePolkadotJs'; +import { useClaimVestedAmountMutation } from '../../../../../hooks/vesting/useClaimVestedAmountMutation'; +import { estimateClaimVesting } from '../../../../../hooks/vesting/useVestingMutationResolvers'; +import { Notification } from '../../../WalletPage'; +import './VestingClaim.scss'; + +export const VestingClaim = ({ + vesting, + setNotification, +}: { + vesting?: Maybe; + setNotification: (notification: Notification) => void; +}) => { + const isVestingAvailable = useMemo(() => { + return ( + vesting?.originalLockBalance && + new BigNumber(vesting?.originalLockBalance).gt('0') + ); + }, [vesting]); + const clearNotificationIntervalRef = useRef(); + const [claimVestedAmount] = useClaimVestedAmountMutation({ + onCompleted: () => { + setNotification('success'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + onError: () => { + setNotification('failed'); + clearNotificationIntervalRef.current = setTimeout(() => { + setNotification('standby'); + }, 4000); + }, + }); + + // TODO: run mutation with confirmation + const handleClaimClick = useCallback(() => { + setNotification('pending'); + claimVestedAmount(); + }, []); + + const { apiInstance, loading: apiInstanceLoading } = usePolkadotJsContext(); + const client = useApolloClient(); + const { feePaymentAsset, convertToFeePaymentAsset } = + useMultiFeePaymentConversionContext(); + + const [txFee, setTxFee] = useState(); + useEffect(() => { + if (!apiInstance || apiInstanceLoading) return; + (async () => { + const txFee = await estimateClaimVesting( + client.cache as any, + apiInstance, + {} + ); + console.log( + 'claim tx fee', + convertToFeePaymentAsset(txFee.partialFee.toString()) + ); + setTxFee(convertToFeePaymentAsset(txFee.partialFee.toString())); + })(); + }, [ + apiInstance, + apiInstanceLoading, + estimateClaimVesting, + client, + convertToFeePaymentAsset, + feePaymentAsset + ]); + + return ( +
+

Vesting

+
+
Claimable
+
Original vesting
+
Remaining vesting
+
Tx fee
+
+
+ {isVestingAvailable ? ( +
+
+ {fromPrecision12(vesting?.claimableAmount)} BSX +
+
+ {fromPrecision12(vesting?.originalLockBalance)} BSX +
+
+ {fromPrecision12(vesting?.lockedVestingBalance)} BSX +
+
+ {txFee ? ( + + ) : ( + <>- + )} +
+
+ +
+
+ ) : ( +
+ <>No vesting available +
+ )} +
+ ); +}; diff --git a/src/schema.graphql b/src/schema.graphql index 4cebd5fd..e52d212e 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,20 +1,18 @@ #import './hooks/accounts/graphql/Accounts.graphql' #import './hooks/lastBlock/graphql/LastBlock.graphql' #import './hooks/config/graphql/Config.graphql' -#import './hooks/vesting/graphql/VestingSchedule.graphql' +#import './hooks/vesting/graphql/Vesting.graphql' #import './hooks/extension/graphql/Extension.graphql' #import './hooks/feePaymentAssets/graphql/FeePaymentAssets.graphql' #import './hooks/pools/graphql/Pool.graphql' #import './hooks/assets/graphql/Asset.graphql' #import './hooks/balances/graphql/LockedBalance.graphql' -# directive @client on FIELD - -# type Query { -# # just a placeholder to make the codegen not complain about -# # root query not being defined -# _empty: String -# } +type Query { + # just a placeholder to make the codegen not complain about + # root query not being defined + _empty: String +} type Mutation { # just a placeholder to make the codegen not complain about diff --git a/yarn.lock b/yarn.lock index 661d4220..64fae68e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4795,6 +4795,13 @@ dependencies: "@types/react" "*" +"@types/react-page-visibility@^6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@types/react-page-visibility/-/react-page-visibility-6.4.1.tgz#21c3bc4a3f310d38d188916cadc55f2bde65f27d" + integrity sha512-vNlYAqKhB2SU1HmF9ARFTFZN0NSPzWn8HSjBpFqYuQlJhsb/aSYeIZdygeqfSjAg0PZ70id2IFWHGULJwe59Aw== + dependencies: + "@types/react" "*" + "@types/react-syntax-highlighter@11.0.5": version "11.0.5" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz#0d546261b4021e1f9d85b50401c0a42acb106087" @@ -11226,9 +11233,9 @@ husky@^7.0.4: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== -"hydra-dx-wasm@https://github.com/galacticcouncil/HydraDX-wasm#main": +"hydra-dx-wasm@https://github.com/galacticcouncil/HydraDX-wasm#0e3d625c22c32525a4619047223cac019c0cfa46": version "3.0.0" - resolved "https://github.com/galacticcouncil/HydraDX-wasm#4451b1dfdef924aab07de97e639b0adf28b5109e" + resolved "https://github.com/galacticcouncil/HydraDX-wasm#0e3d625c22c32525a4619047223cac019c0cfa46" hyphenate-style-name@^1.0.2: version "1.0.4" @@ -16261,6 +16268,13 @@ react-multi-provider@^0.1.5: resolved "https://registry.yarnpkg.com/react-multi-provider/-/react-multi-provider-0.1.5.tgz#fd712b2340eca3311273fdd9e8e8efd3ac31d7e2" integrity sha512-eJWrjtSPIXZKQxN6ieNPb1TaZHlHSqWFBrwAgvCrhRd2GIFVJqZz08/atQSY421kMVeravzFZv1HfiLbrltiZA== +react-page-visibility@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/react-page-visibility/-/react-page-visibility-6.4.0.tgz#0684fe80338e716c9ed2d34169fa3cbb3882096b" + integrity sha512-5vQ0zQU2DvKCQAxle9l5V6uxw2m180Lk7Jem+obmTeQ503fvMJLSUzFgWtTEgUVynhUx2pd+RzafnuMAG8uD6A== + dependencies: + prop-types "^15.7.2" + react-popper-tooltip@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-3.1.1.tgz#329569eb7b287008f04fcbddb6370452ad3f9eac"